File indexing completed on 2024-04-14 04:46:28
0001 /* 0002 SPDX-FileCopyrightText: 2008 Jean-Baptiste Mardelle <jb@kdenlive.org> 0003 0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 #include "documentchecker.h" 0007 #include "bin/binplaylist.hpp" 0008 #include "bin/projectclip.h" 0009 #include "dcresolvedialog.h" 0010 #include "effects/effectsrepository.hpp" 0011 #include "kdenlivesettings.h" 0012 #include "titler/titlewidget.h" 0013 #include "transitions/transitionsrepository.hpp" 0014 #include "xml/xml.hpp" 0015 0016 #include <KLocalizedString> 0017 0018 #include <QCryptographicHash> 0019 #include <QStandardPaths> 0020 0021 QDebug operator<<(QDebug qd, const DocumentChecker::DocumentResource &item) 0022 { 0023 qd << "Type:" << DocumentChecker::readableNameForMissingType(item.type); 0024 qd << "Status:" << DocumentChecker::readableNameForMissingStatus(item.status); 0025 qd << "Original Paths:" << item.originalFilePath; 0026 qd << "New Path:" << item.newFilePath; 0027 qd << "clipID:" << item.clipId; 0028 return qd.maybeSpace(); 0029 } 0030 0031 DocumentChecker::DocumentChecker(QUrl url, const QDomDocument &doc) 0032 : m_url(std::move(url)) 0033 , m_doc(doc) 0034 { 0035 0036 QDomElement baseElement = m_doc.documentElement(); 0037 m_root = baseElement.attribute(QStringLiteral("root")); 0038 if (m_root.isEmpty() || !QDir(m_root).exists()) { 0039 // Looks like project was moved, try recovering root from current project url 0040 m_rootReplacement.first = QDir(m_root).absolutePath() + QDir::separator(); 0041 m_root = m_url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(); 0042 baseElement.setAttribute(QStringLiteral("root"), m_root); 0043 m_root = QDir::cleanPath(m_root) + QDir::separator(); 0044 m_rootReplacement.second = m_root; 0045 } 0046 if (!m_root.isEmpty() && QDir(m_root).exists()) { 0047 m_root = QDir::cleanPath(m_root) + QDir::separator(); 0048 } 0049 } 0050 0051 const QMap<QString, QString> DocumentChecker::getLumaPairs() const 0052 { 0053 QMap<QString, QString> lumaSearchPairs; 0054 lumaSearchPairs.insert(QStringLiteral("luma"), QStringLiteral("resource")); 0055 lumaSearchPairs.insert(QStringLiteral("movit.luma_mix"), QStringLiteral("resource")); 0056 lumaSearchPairs.insert(QStringLiteral("composite"), QStringLiteral("luma")); 0057 lumaSearchPairs.insert(QStringLiteral("region"), QStringLiteral("composite.luma")); 0058 return lumaSearchPairs; 0059 } 0060 0061 const QMap<QString, QString> DocumentChecker::getAssetPairs() const 0062 { 0063 QMap<QString, QString> assetSearchPairs; 0064 assetSearchPairs.insert(QStringLiteral("avfilter.lut3d"), QStringLiteral("av.file")); 0065 assetSearchPairs.insert(QStringLiteral("shape"), QStringLiteral("resource")); 0066 return assetSearchPairs; 0067 } 0068 0069 bool DocumentChecker::resolveProblemsWithGUI() 0070 { 0071 if (m_items.size() == 0) { 0072 return true; 0073 } 0074 0075 bool onlySilent = true; 0076 for (auto item : m_items) { 0077 if (item.status != MissingStatus::Fixed || item.type != MissingType::AssetFile) { 0078 // we don't need to warn about automatic asset file fixes 0079 onlySilent = false; 0080 break; 0081 } 0082 } 0083 0084 if (onlySilent) { 0085 return true; 0086 } 0087 0088 DCResolveDialog *d = new DCResolveDialog(m_items, m_url); 0089 // d->show(getInfoMessages()); 0090 if (d->exec() == QDialog::Rejected) { 0091 return false; 0092 } 0093 0094 QList<DocumentResource> items = d->getItems(); 0095 0096 QDomNodeList producers = m_doc.elementsByTagName(QStringLiteral("producer")); 0097 QDomNodeList chains = m_doc.elementsByTagName(QStringLiteral("chain")); 0098 0099 QDomNodeList trans = m_doc.elementsByTagName(QStringLiteral("transition")); 0100 QDomNodeList filters = m_doc.elementsByTagName(QStringLiteral("filter")); 0101 0102 QDomNodeList documentTractors = m_doc.elementsByTagName(QStringLiteral("tractor")); 0103 0104 const int taskCount = items.count() + documentTractors.count() + producers.count() + chains.count(); 0105 0106 Q_EMIT pCore->loadingMessageNewStage(i18n("Applying fixes…"), taskCount); 0107 0108 for (auto item : items) { 0109 fixMissingItem(item, producers, chains, trans, filters); 0110 Q_EMIT pCore->loadingMessageIncrease(); 0111 qApp->processEvents(); 0112 } 0113 0114 QStringList tractorIds; 0115 int max = documentTractors.count(); 0116 for (int i = 0; i < max; ++i) { 0117 QDomElement tractor = documentTractors.item(i).toElement(); 0118 tractorIds.append(tractor.attribute(QStringLiteral("id"))); 0119 Q_EMIT pCore->loadingMessageIncrease(); 0120 qApp->processEvents(); 0121 } 0122 0123 max = producers.count(); 0124 for (int i = 0; i < max; ++i) { 0125 QDomElement e = producers.item(i).toElement(); 0126 fixSequences(e, producers, tractorIds); 0127 Q_EMIT pCore->loadingMessageIncrease(); 0128 qApp->processEvents(); 0129 } 0130 max = chains.count(); 0131 for (int i = 0; i < max; ++i) { 0132 QDomElement e = chains.item(i).toElement(); 0133 fixSequences(e, producers, tractorIds); 0134 Q_EMIT pCore->loadingMessageIncrease(); 0135 qApp->processEvents(); 0136 } 0137 0138 // original doc was modified 0139 m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); 0140 return true; 0141 } 0142 0143 bool DocumentChecker::hasErrorInProject() 0144 { 0145 Q_EMIT pCore->loadingMessageNewStage(i18n("Checking for missing items…"), 0); 0146 m_items.clear(); 0147 0148 QString storageFolder; 0149 QDir projectDir(m_url.adjusted(QUrl::RemoveFilename).toLocalFile()); 0150 QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); 0151 for (int i = 0; i < playlists.count(); ++i) { 0152 if (playlists.at(i).toElement().attribute(QStringLiteral("id")) == BinPlaylist::binPlaylistId) { 0153 QDomElement mainBinPlaylist = playlists.at(i).toElement(); 0154 0155 // ensure the documentid is valid 0156 m_documentid = Xml::getXmlProperty(mainBinPlaylist, QStringLiteral("kdenlive:docproperties.documentid")); 0157 if (m_documentid.isEmpty()) { 0158 // invalid document id, recreate one 0159 m_documentid = QString::number(QDateTime::currentMSecsSinceEpoch()); 0160 Xml::setXmlProperty(mainBinPlaylist, QStringLiteral("kdenlive:docproperties.documentid"), m_documentid); 0161 m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); 0162 m_warnings.append(i18n("The document id of your project was invalid, a new one has been created.")); 0163 } 0164 0165 // ensure the storage for temp files exists 0166 storageFolder = Xml::getXmlProperty(mainBinPlaylist, QStringLiteral("kdenlive:docproperties.storagefolder")); 0167 storageFolder = ensureAbsolutePath(storageFolder); 0168 0169 if (!storageFolder.isEmpty() && !QFile::exists(storageFolder) && projectDir.exists(m_documentid)) { 0170 storageFolder = projectDir.absolutePath(); 0171 Xml::setXmlProperty(mainBinPlaylist, QStringLiteral("kdenlive:docproperties.storagefolder"), projectDir.absoluteFilePath(m_documentid)); 0172 m_doc.documentElement().setAttribute(QStringLiteral("modified"), 1); 0173 } 0174 0175 // get bin ids 0176 m_binEntries = mainBinPlaylist.elementsByTagName(QLatin1String("entry")); 0177 for (int i = 0; i < m_binEntries.count(); ++i) { 0178 QDomElement e = m_binEntries.item(i).toElement(); 0179 m_binIds << e.attribute(QStringLiteral("producer")); 0180 } 0181 break; 0182 } 0183 } 0184 0185 QDomNodeList documentTractors = m_doc.elementsByTagName(QStringLiteral("tractor")); 0186 QDomNodeList documentProducers = m_doc.elementsByTagName(QStringLiteral("producer")); 0187 QDomNodeList documentChains = m_doc.elementsByTagName(QStringLiteral("chain")); 0188 QDomNodeList entries = m_doc.elementsByTagName(QStringLiteral("entry")); 0189 QDomNodeList transitions = m_doc.elementsByTagName(QStringLiteral("transition")); 0190 QMap<QString, QString> renamedEffects; 0191 renamedEffects.insert(QStringLiteral("frei0r.alpha0ps"), QStringLiteral("frei0r.alpha0ps_alpha0ps")); 0192 renamedEffects.insert(QStringLiteral("frei0r.alphaspot"), QStringLiteral("frei0r.alpha0ps_alphaspot")); 0193 renamedEffects.insert(QStringLiteral("frei0r.alphagrad"), QStringLiteral("frei0r.alpha0ps_alpha0grad")); 0194 0195 m_safeImages.clear(); 0196 m_safeFonts.clear(); 0197 0198 const int taskCount = documentProducers.count() + documentChains.count() + documentTractors.count(); 0199 Q_EMIT pCore->loadingMessageNewStage(i18n("Checking for missing items…"), taskCount); 0200 0201 QStringList verifiedPaths; 0202 int max = documentProducers.count(); 0203 for (int i = 0; i < max; ++i) { 0204 QDomElement e = documentProducers.item(i).toElement(); 0205 verifiedPaths << getMissingProducers(e, entries, storageFolder); 0206 Q_EMIT pCore->loadingMessageIncrease(); 0207 } 0208 max = documentChains.count(); 0209 for (int i = 0; i < max; ++i) { 0210 QDomElement e = documentChains.item(i).toElement(); 0211 verifiedPaths << getMissingProducers(e, entries, storageFolder); 0212 Q_EMIT pCore->loadingMessageIncrease(); 0213 } 0214 // Check that we don't have circular dependencies (a sequence embedding itself as a track / ptoducer 0215 max = documentTractors.count(); 0216 QStringList circularRefs; 0217 for (int i = 0; i < max; ++i) { 0218 Q_EMIT pCore->loadingMessageIncrease(); 0219 QDomElement e = documentTractors.item(i).toElement(); 0220 const QString tractorName = e.attribute(QStringLiteral("id")); 0221 QDomNodeList tracks = e.elementsByTagName(QStringLiteral("track")); 0222 int maxTracks = tracks.count(); 0223 QList<int> tracksToRemove; 0224 for (int j = 0; j < maxTracks; ++j) { 0225 QDomElement tr = tracks.item(j).toElement(); 0226 if (tr.attribute(QStringLiteral("producer")) == tractorName) { 0227 // Malformed track, should be removed from project 0228 tracksToRemove << j; 0229 continue; 0230 } 0231 } 0232 while (!tracksToRemove.isEmpty()) { 0233 // Process removal from end 0234 int x = tracksToRemove.takeLast(); 0235 QDomNode nodeToRemove = tracks.item(x); 0236 e.removeChild(nodeToRemove); 0237 circularRefs << tractorName; 0238 } 0239 } 0240 if (!circularRefs.isEmpty()) { 0241 circularRefs.removeDuplicates(); 0242 DocumentResource item; 0243 item.type = MissingType::CircularRef; 0244 item.status = MissingStatus::Remove; 0245 item.originalFilePath = circularRefs.join(QLatin1Char(',')); 0246 m_items.push_back(item); 0247 } 0248 0249 // Check existence of luma files 0250 QStringList filesToCheck = getAssetsFilesByMltTag(m_doc, QStringLiteral("transition"), getLumaPairs()); 0251 for (const QString &lumafile : qAsConst(filesToCheck)) { 0252 QString filePath = ensureAbsolutePath(lumafile); 0253 0254 if (QFile::exists(filePath)) { 0255 // everything is fine, we can stop here 0256 continue; 0257 } 0258 0259 QString lumaName = QFileInfo(filePath).fileName(); 0260 // MLT 7 now generates lumas on the fly, so don't detect these as missing 0261 if (isMltBuildInLuma(lumaName)) { 0262 // everything is fine, we can stop here 0263 continue; 0264 } 0265 0266 // check if this was an old format luma, not in correct folder 0267 QString fixedLuma = filePath.section(QLatin1Char('/'), 0, -2); 0268 lumaName.prepend(isProfileHD(m_doc) ? QStringLiteral("/HD/") : QStringLiteral("/PAL/")); 0269 fixedLuma.append(lumaName); 0270 0271 if (!QFile::exists(fixedLuma)) { 0272 // Check Kdenlive folder 0273 fixedLuma = fixLumaPath(filePath); 0274 } 0275 0276 if (!QFile::exists(fixedLuma)) { 0277 // Try to change file extension 0278 if (filePath.endsWith(QLatin1String(".pgm"))) { 0279 fixedLuma = filePath.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".png"); 0280 } else if (filePath.endsWith(QLatin1String(".png"))) { 0281 fixedLuma = filePath.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".pgm"); 0282 } 0283 } 0284 0285 DocumentResource item; 0286 item.type = MissingType::Luma; 0287 item.originalFilePath = filePath; 0288 0289 if (QFile::exists(fixedLuma)) { 0290 if (filePath.startsWith(QStringLiteral("/tmp/.mount_"))) { 0291 // This is a luma in the Appimage, fix silently 0292 fixAssetResource(transitions, getLumaPairs(), filePath, fixedLuma); 0293 continue; 0294 } 0295 item.newFilePath = fixedLuma; 0296 item.status = MissingStatus::Fixed; 0297 } else { 0298 // we have not been able to fix or find the file 0299 item.status = MissingStatus::Missing; 0300 } 0301 0302 if (!itemsContain(item.type, item.originalFilePath, item.status)) { 0303 m_items.push_back(item); 0304 } 0305 } 0306 0307 // Check for missing transitions (eg. not installed) 0308 QStringList transtions = getAssetsServiceIds(m_doc, QStringLiteral("transition")); 0309 for (const QString &id : qAsConst(transtions)) { 0310 if (!TransitionsRepository::get()->exists(id) && !itemsContain(MissingType::Transition, id, MissingStatus::Remove)) { 0311 DocumentResource item; 0312 item.type = MissingType::Transition; 0313 item.status = MissingStatus::Remove; 0314 item.originalFilePath = id; 0315 m_items.push_back(item); 0316 } 0317 } 0318 0319 // Check for missing filter assets 0320 QStringList assetsToCheck = getAssetsFilesByMltTag(m_doc, QStringLiteral("filter"), getAssetPairs()); 0321 for (const QString &filterfile : qAsConst(assetsToCheck)) { 0322 QString filePath = ensureAbsolutePath(filterfile); 0323 0324 if (QFile::exists(filePath)) { 0325 // everything is fine, we can stop here 0326 continue; 0327 } 0328 0329 QString fixedPath = fixLutFile(filePath); 0330 0331 DocumentResource item; 0332 item.type = MissingType::AssetFile; 0333 item.originalFilePath = filePath; 0334 0335 if (!fixedPath.isEmpty()) { 0336 item.newFilePath = fixedPath; 0337 item.status = MissingStatus::Fixed; 0338 } else { 0339 item.status = MissingStatus::Missing; 0340 } 0341 0342 if (!itemsContain(item.type, item.originalFilePath, item.status)) { 0343 m_items.push_back(item); 0344 } 0345 } 0346 0347 // Check for missing effects (eg. not installed) 0348 QStringList filters = getAssetsServiceIds(m_doc, QStringLiteral("filter")); 0349 QStringList renamedEffectNames = renamedEffects.keys(); 0350 for (const QString &id : qAsConst(filters)) { 0351 if (!EffectsRepository::get()->exists(id) && !itemsContain(MissingType::Effect, id, MissingStatus::Remove)) { 0352 // m_missingFilters << id; 0353 if (renamedEffectNames.contains(id) && EffectsRepository::get()->exists(renamedEffects.value(id))) { 0354 // The effect was renamed 0355 DocumentResource item; 0356 item.type = MissingType::Effect; 0357 item.status = MissingStatus::Fixed; 0358 item.originalFilePath = id; 0359 item.newFilePath = renamedEffects.value(id); 0360 m_items.push_back(item); 0361 continue; 0362 } 0363 DocumentResource item; 0364 item.type = MissingType::Effect; 0365 item.status = MissingStatus::Remove; 0366 item.originalFilePath = id; 0367 m_items.push_back(item); 0368 } 0369 } 0370 0371 if (m_items.size() == 0) { 0372 return false; 0373 } 0374 return true; 0375 } 0376 0377 DocumentChecker::~DocumentChecker() {} 0378 0379 const QString DocumentChecker::relocateResource(QString sourceResource) 0380 { 0381 if (m_rootReplacement.first.isEmpty()) { 0382 return QString(); 0383 } 0384 0385 if (sourceResource.startsWith(m_rootReplacement.first)) { 0386 sourceResource.replace(m_rootReplacement.first, m_rootReplacement.second); 0387 // Use QFileInfo to ensure we also handle directories (for slideshows) 0388 if (QFileInfo::exists(sourceResource)) { 0389 return sourceResource; 0390 } 0391 return QString(); 0392 } 0393 // Check if we have a common root, if file has a common ancestor in its path 0394 QStringList replacedRoot = m_rootReplacement.second.split(QLatin1Char('/')); 0395 QStringList cutRoot = m_rootReplacement.first.split(QLatin1Char('/')); 0396 QStringList cutResource = sourceResource.split(QLatin1Char('/')); 0397 // Find common ancestor 0398 int ix = 0; 0399 for (auto &cut : cutRoot) { 0400 if (!cutResource.isEmpty()) { 0401 if (cutResource.first() != cut) { 0402 break; 0403 } 0404 } else { 0405 break; 0406 } 0407 cutResource.takeFirst(); 0408 ix++; 0409 } 0410 int diff = cutRoot.size() - ix; 0411 if (diff < replacedRoot.size()) { 0412 while (diff > 0) { 0413 replacedRoot.removeLast(); 0414 diff--; 0415 } 0416 } 0417 QString basePath = replacedRoot.join(QLatin1Char('/')); 0418 basePath.append(QLatin1Char('/')); 0419 basePath.append(cutResource.join(QLatin1Char('/'))); 0420 qDebug() << "/// RESULTING PATH: " << basePath; 0421 // Use QFileInfo to ensure we also handle directories (for slideshows) 0422 if (QFileInfo::exists(basePath)) { 0423 return basePath; 0424 } 0425 return QString(); 0426 } 0427 0428 bool DocumentChecker::ensureProducerHasId(QDomElement &producer, const QDomNodeList &entries) 0429 { 0430 if (!Xml::getXmlProperty(producer, QStringLiteral("kdenlive:id")).isEmpty()) { 0431 // id is there, everything is fine 0432 return false; 0433 } 0434 0435 // This should not happen, try to recover the producer id 0436 int max = entries.count(); 0437 QString producerName = producer.attribute(QStringLiteral("id")); 0438 for (int j = 0; j < max; j++) { 0439 QDomElement e = entries.item(j).toElement(); 0440 if (e.attribute(QStringLiteral("producer")) == producerName) { 0441 // Match found 0442 QString entryName = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); 0443 if (!entryName.isEmpty()) { 0444 Xml::setXmlProperty(producer, QStringLiteral("kdenlive:id"), entryName); 0445 return true; 0446 } 0447 } 0448 } 0449 return false; 0450 } 0451 0452 bool DocumentChecker::ensureProducerIsNotPlaceholder(QDomElement &producer) 0453 { 0454 QString text = Xml::getXmlProperty(producer, QStringLiteral("text")); 0455 QString service = Xml::getXmlProperty(producer, QStringLiteral("mlt_service")); 0456 0457 // Check if this is an invalid clip (project saved with missing source) 0458 if (service != QLatin1String("qtext") || text != QLatin1String("INVALID")) { 0459 // This does not seem to be a placeholder for an invalid clip 0460 return false; 0461 } 0462 0463 // Clip saved with missing source: check if source clip is now available 0464 QString resource = Xml::getXmlProperty(producer, QStringLiteral("warp_resource")); 0465 if (resource.isEmpty()) { 0466 resource = Xml::getXmlProperty(producer, QStringLiteral("resource")); 0467 } 0468 resource = ensureAbsolutePath(resource); 0469 0470 if (!QFile::exists(resource)) { 0471 // The source clip is still not available 0472 return false; 0473 } 0474 0475 // Reset to original service 0476 Xml::removeXmlProperty(producer, QStringLiteral("text")); 0477 QString original_service = Xml::getXmlProperty(producer, QStringLiteral("kdenlive:orig_service")); 0478 if (!original_service.isEmpty()) { 0479 Xml::setXmlProperty(producer, QStringLiteral("mlt_service"), original_service); 0480 // We know the original service and recovered it, everything is fine again 0481 return true; 0482 } 0483 0484 // Try to guess service as we do not know it 0485 QString guessedService; 0486 if (Xml::hasXmlProperty(producer, QStringLiteral("ttl"))) { 0487 guessedService = QStringLiteral("qimage"); 0488 } else if (resource.endsWith(QLatin1String(".kdenlivetitle"))) { 0489 guessedService = QStringLiteral("kdenlivetitle"); 0490 } else if (resource.endsWith(QLatin1String(".kdenlive")) || resource.endsWith(QLatin1String(".mlt"))) { 0491 guessedService = QStringLiteral("xml"); 0492 } else { 0493 guessedService = QStringLiteral("avformat"); 0494 } 0495 Xml::setXmlProperty(producer, QStringLiteral("mlt_service"), guessedService); 0496 return true; 0497 } 0498 0499 /*void DocumentChecker::setReloadProxy(QDomElement &producer, const QString &realPath) 0500 { 0501 // Tell Kdenlive to recreate proxy 0502 producer.setAttribute(QStringLiteral("_replaceproxy"), QStringLiteral("1")); 0503 // Remove reference to missing proxy 0504 Xml::setXmlProperty(producer, QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); 0505 0506 // Replace proxy url with real clip in MLT producers 0507 QString prefix; 0508 QString originalService = Xml::getXmlProperty(producer, QStringLiteral("kdenlive:original.mlt_service")); 0509 QString service = Xml::getXmlProperty(producer, QStringLiteral("mlt_service")); 0510 if (service == QLatin1String("timewarp")) { 0511 prefix = Xml::getXmlProperty(producer, QStringLiteral("warp_speed")); 0512 prefix.append(QLatin1Char(':')); 0513 Xml::setXmlProperty(producer, QStringLiteral("warp_resource"), prefix + realPath); 0514 } else if (!originalService.isEmpty()) { 0515 Xml::setXmlProperty(producer, QStringLiteral("mlt_service"), originalService); 0516 } 0517 prefix.append(realPath); 0518 Xml::setXmlProperty(producer, QStringLiteral("resource"), prefix); 0519 }*/ 0520 0521 void DocumentChecker::removeProxy(const QDomNodeList &items, const QString &clipId, bool recreate) 0522 { 0523 QDomElement e; 0524 for (int i = 0; i < items.count(); ++i) { 0525 e = items.item(i).toElement(); 0526 QString parentId = getKdenliveClipId(e); 0527 if (parentId != clipId) { 0528 continue; 0529 } 0530 // Tell Kdenlive to recreate proxy 0531 if (recreate) { 0532 e.setAttribute(QStringLiteral("_replaceproxy"), QStringLiteral("1")); 0533 } 0534 // Remove reference to missing proxy 0535 Xml::setXmlProperty(e, QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); 0536 0537 // Replace proxy url with real clip in MLT producers 0538 QString prefix; 0539 const QString originalService = Xml::getXmlProperty(e, QStringLiteral("kdenlive:original.mlt_service")); 0540 const QString originalPath = Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")); 0541 if (originalPath.isEmpty()) { 0542 // The clip proxy process was not completed, leave resource untouched 0543 return; 0544 } 0545 QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); 0546 if (service == QLatin1String("timewarp")) { 0547 prefix = Xml::getXmlProperty(e, QStringLiteral("warp_speed")); 0548 prefix.append(QLatin1Char(':')); 0549 Xml::setXmlProperty(e, QStringLiteral("warp_resource"), prefix + originalPath); 0550 } else if (!originalService.isEmpty()) { 0551 if (originalService == QLatin1String("xml")) { 0552 e.setTagName(QStringLiteral("producer")); 0553 } 0554 Xml::setXmlProperty(e, QStringLiteral("mlt_service"), originalService); 0555 } 0556 prefix.append(originalPath); 0557 Xml::setXmlProperty(e, QStringLiteral("resource"), prefix); 0558 } 0559 } 0560 0561 void DocumentChecker::checkMissingImagesAndFonts(const QStringList &images, const QStringList &fonts, const QString &id) 0562 { 0563 for (const QString &img : images) { 0564 if (m_safeImages.contains(img)) { 0565 continue; 0566 } 0567 if (!QFile::exists(img)) { 0568 DocumentResource item; 0569 item.type = MissingType::TitleImage; 0570 item.status = MissingStatus::Missing; 0571 item.originalFilePath = img; 0572 item.clipId = id; 0573 m_items.push_back(item); 0574 0575 const QString relocated = relocateResource(img); 0576 if (!relocated.isEmpty()) { 0577 item.status = MissingStatus::Fixed; 0578 item.newFilePath = relocated; 0579 } 0580 } else { 0581 m_safeImages.append(img); 0582 } 0583 } 0584 for (const QString &fontelement : fonts) { 0585 if (m_safeFonts.contains(fontelement) || itemsContain(MissingType::TitleFont, fontelement)) { 0586 continue; 0587 } 0588 QFont f(fontelement); 0589 if (fontelement != QFontInfo(f).family()) { 0590 DocumentResource item; 0591 item.type = MissingType::TitleFont; 0592 item.originalFilePath = fontelement; 0593 item.newFilePath = QFontInfo(f).family(); 0594 item.status = MissingStatus::Placeholder; 0595 m_items.push_back(item); 0596 } else { 0597 m_safeFonts.append(fontelement); 0598 } 0599 } 0600 } 0601 0602 QString DocumentChecker::getMissingProducers(QDomElement &e, const QDomNodeList &entries, const QString &storageFolder) 0603 { 0604 QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); 0605 QStringList serviceToCheck = {QStringLiteral("kdenlivetitle"), QStringLiteral("qimage"), QStringLiteral("pixbuf"), QStringLiteral("timewarp"), 0606 QStringLiteral("framebuffer"), QStringLiteral("xml"), QStringLiteral("qtext"), QStringLiteral("tractor"), 0607 QStringLiteral("glaxnimate"), QStringLiteral("consumer")}; 0608 if (!service.startsWith(QLatin1String("avformat")) && !serviceToCheck.contains(service)) { 0609 return QString(); 0610 } 0611 0612 ensureProducerHasId(e, entries); 0613 0614 if (ensureProducerIsNotPlaceholder(e)) { 0615 return QString(); 0616 } 0617 0618 bool isBinClip = m_binIds.contains(e.attribute(QLatin1String("id"))); 0619 0620 if (service == QLatin1String("qtext")) { 0621 checkMissingImagesAndFonts(QStringList(), QStringList(Xml::getXmlProperty(e, QStringLiteral("family"))), e.attribute(QStringLiteral("id"))); 0622 return QString(); 0623 } else if (service == QLatin1String("kdenlivetitle")) { 0624 // TODO: Check if clip template is missing (xmltemplate) or hash changed 0625 QPair<QStringList, QStringList> titlesList = TitleWidget::extractAndFixImageList(e, m_root); 0626 checkMissingImagesAndFonts(titlesList.first, titlesList.second, Xml::getXmlProperty(e, QStringLiteral("kdenlive:id"))); 0627 return QString(); 0628 } 0629 0630 QString clipId = getKdenliveClipId(e); 0631 QString resource = getProducerResource(e); 0632 ClipType::ProducerType clipType = getClipType(service, resource); 0633 int index = itemIndexByClipId(clipId); 0634 if (index > -1) { 0635 if (m_items[index].hash.isEmpty()) { 0636 m_items[index].hash = Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_hash")); 0637 m_items[index].fileSize = Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_size")); 0638 } 0639 } 0640 0641 auto checkClip = [this, clipId, clipType, isBinClip](QDomElement &e, const QString &resource) { 0642 if (isSequenceWithSpeedEffect(e)) { 0643 // This is a missing timeline sequence clip with speed effect, trigger recreate on opening 0644 Xml::setXmlProperty(e, QStringLiteral("_rebuild"), QStringLiteral("1")); 0645 // missingPaths.append(resource); 0646 } else if (isBinClip) { 0647 DocumentResource item; 0648 item.status = MissingStatus::Missing; 0649 item.clipId = clipId; 0650 item.clipType = clipType; 0651 item.originalFilePath = resource; 0652 item.type = MissingType::Clip; 0653 item.hash = Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_hash")); 0654 item.fileSize = Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_size")); 0655 0656 QString relocated; 0657 if (clipType == ClipType::SlideShow) { 0658 // Strip filename 0659 relocated = QFileInfo(resource).absolutePath(); 0660 } else { 0661 relocated = resource; 0662 } 0663 relocated = relocateResource(relocated); 0664 if (!relocated.isEmpty()) { 0665 if (clipType == ClipType::SlideShow) { 0666 item.newFilePath = QDir(relocated).absoluteFilePath(QFileInfo(resource).fileName()); 0667 } else { 0668 item.newFilePath = relocated; 0669 } 0670 item.newFilePath = relocated; 0671 item.status = MissingStatus::Fixed; 0672 } 0673 m_items.push_back(item); 0674 } 0675 }; 0676 0677 // If 2 clips share the same resource url, we need to mark both as missing 0678 /*if (!resource.isEmpty() && verifiedPaths.contains(resource)) { 0679 // Don't check same url twice (for example track producers) 0680 return QString(); 0681 }*/ 0682 QString producerResource = resource; 0683 QString proxy = Xml::getXmlProperty(e, QStringLiteral("kdenlive:proxy")); 0684 if (isBinClip && !proxy.isEmpty() && proxy.length() > 1) { 0685 bool proxyFound = true; 0686 proxy = ensureAbsolutePath(proxy); 0687 if (!QFile::exists(proxy)) { 0688 // Missing clip found 0689 // Check if proxy exists in current storage folder 0690 bool fixed = false; 0691 if (!storageFolder.isEmpty()) { 0692 QDir dir(storageFolder + QStringLiteral("/proxy/")); 0693 if (dir.exists(QFileInfo(proxy).fileName())) { 0694 QString updatedPath = dir.absoluteFilePath(QFileInfo(proxy).fileName()); 0695 DocumentResource item; 0696 item.clipId = clipId; 0697 item.clipType = clipType; 0698 item.status = MissingStatus::Fixed; 0699 item.type = MissingType::Proxy; 0700 item.originalFilePath = proxy; 0701 item.newFilePath = updatedPath; 0702 m_items.push_back(item); 0703 fixed = true; 0704 } 0705 } 0706 if (!fixed) { 0707 proxyFound = false; 0708 } 0709 } 0710 QString original = Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")); 0711 original = ensureAbsolutePath(original); 0712 0713 // Check for slideshows 0714 bool slideshow = isSlideshow(original); 0715 if (slideshow && Xml::hasXmlProperty(e, QStringLiteral("ttl"))) { 0716 original = QFileInfo(original).absolutePath(); 0717 } 0718 DocumentResource item; 0719 item.clipId = clipId; 0720 item.clipType = clipType; 0721 item.status = MissingStatus::Missing; 0722 if (!QFile::exists(original)) { 0723 bool resourceFixed = false; 0724 QString movedOriginal = relocateResource(original); 0725 if (!movedOriginal.isEmpty()) { 0726 if (slideshow) { 0727 movedOriginal = QDir(movedOriginal).absoluteFilePath(QFileInfo(original).fileName()); 0728 } 0729 Xml::setXmlProperty(e, QStringLiteral("kdenlive:originalurl"), movedOriginal); 0730 if (!QFile::exists(producerResource)) { 0731 Xml::setXmlProperty(e, QStringLiteral("resource"), movedOriginal); 0732 } 0733 resourceFixed = true; 0734 if (proxyFound) { 0735 return QString(); 0736 } 0737 } 0738 0739 if (!proxyFound) { 0740 item.originalFilePath = proxy; 0741 item.type = MissingType::Proxy; 0742 0743 if (!resourceFixed) { 0744 // Neither proxy nor original file found 0745 checkClip(e, original); 0746 } 0747 } else { 0748 // clip has proxy but original clip is missing 0749 item.originalFilePath = original; 0750 item.type = MissingType::Clip; 0751 item.status = MissingStatus::MissingButProxy; 0752 // e.setAttribute(QStringLiteral("_missingsource"), QStringLiteral("1")); 0753 item.hash = Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_hash")); 0754 item.fileSize = Xml::getXmlProperty(e, QStringLiteral("kdenlive:file_size")); 0755 // item.mltService = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); 0756 } 0757 m_items.push_back(item); 0758 } else if (!proxyFound) { 0759 item.originalFilePath = proxy; 0760 item.type = MissingType::Proxy; 0761 m_items.push_back(item); 0762 } 0763 return resource; 0764 } 0765 0766 // Check for slideshows 0767 QString slidePattern; 0768 bool slideshow = isSlideshow(resource); 0769 if (slideshow) { 0770 if (service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) { 0771 slidePattern = QFileInfo(resource).fileName(); 0772 resource = QFileInfo(resource).absolutePath(); 0773 } else if ((service.startsWith(QLatin1String("avformat")) || service == QLatin1String("timewarp")) && Xml::hasXmlProperty(e, QStringLiteral("ttl"))) { 0774 // Fix MLT 6.20 avformat slideshows 0775 if (service.startsWith(QLatin1String("avformat"))) { 0776 Xml::setXmlProperty(e, QStringLiteral("mlt_service"), QStringLiteral("qimage")); 0777 } 0778 slidePattern = QFileInfo(resource).fileName(); 0779 resource = QFileInfo(resource).absolutePath(); 0780 } else { 0781 slideshow = false; 0782 } 0783 } 0784 const QStringList checkHashForService = {QLatin1String("qimage"), QLatin1String("pixbuf"), QLatin1String("glaxnimate")}; 0785 if (!QFile::exists(resource)) { 0786 if (service == QLatin1String("timewarp") && proxy == QLatin1String("-")) { 0787 // In some corrupted cases, clips with speed effect kept a reference to proxy clip in warp_resource 0788 QString original = Xml::getXmlProperty(e, QStringLiteral("kdenlive:originalurl")); 0789 original = ensureAbsolutePath(original); 0790 if (original != resource && QFile::exists(original)) { 0791 // Fix timewarp producer 0792 Xml::setXmlProperty(e, QStringLiteral("warp_resource"), original); 0793 Xml::setXmlProperty(e, QStringLiteral("resource"), Xml::getXmlProperty(e, QStringLiteral("warp_speed")) + QStringLiteral(":") + original); 0794 return original; 0795 } 0796 } 0797 bool isPreviewChunk = QFileInfo(resource).absolutePath().endsWith(QString("/%1/preview").arg(m_documentid)); 0798 // Missing clip found, make sure to omit timeline preview 0799 if (!isPreviewChunk) { 0800 checkClip(e, resource); 0801 } 0802 } else if (isBinClip && (service.startsWith(QLatin1String("avformat")) || slideshow || checkHashForService.contains(service))) { 0803 // Check if file changed 0804 const QByteArray hash = Xml::getXmlProperty(e, "kdenlive:file_hash").toLatin1(); 0805 if (!hash.isEmpty()) { 0806 const QByteArray fileData = 0807 slideshow ? ProjectClip::getFolderHash(QDir(resource), slidePattern).toHex() : ProjectClip::calculateHash(resource).first.toHex(); 0808 if (hash != fileData) { 0809 if (slideshow) { 0810 // For slideshow clips, silently upgrade hash 0811 Xml::setXmlProperty(e, "kdenlive:file_hash", fileData); 0812 } else { 0813 // Clip was changed, notify and trigger clip reload 0814 Xml::removeXmlProperty(e, "kdenlive:file_hash"); 0815 DocumentResource item; 0816 item.originalFilePath = resource; 0817 item.clipId = clipId; 0818 item.clipType = clipType; 0819 item.type = MissingType::Clip; 0820 item.status = MissingStatus::Reload; 0821 m_items.push_back(item); 0822 } 0823 } 0824 } 0825 } 0826 // Make sure we don't query same path twice 0827 return producerResource; 0828 } 0829 0830 QString DocumentChecker::fixLutFile(const QString &file) 0831 { 0832 QDir searchPath(QCoreApplication::applicationDirPath()); 0833 #ifdef Q_OS_WIN 0834 searchPath.cd(QStringLiteral("data/luts/")); 0835 #else 0836 searchPath.cd(QStringLiteral("../share/kdenlive/luts/")); 0837 #endif 0838 QString fname = QFileInfo(file).fileName(); 0839 QFileInfo result(searchPath, fname); 0840 if (result.exists()) { 0841 return result.filePath(); 0842 } 0843 // Try in Kdenlive's standard KDE path 0844 QStringList resList = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, "luts", QStandardPaths::LocateDirectory); 0845 for (auto res : resList) { 0846 if (!res.isEmpty()) { 0847 searchPath.setPath(res); 0848 result.setFile(searchPath, fname); 0849 if (result.exists()) { 0850 return result.filePath(); 0851 } 0852 } 0853 } 0854 return QString(); 0855 } 0856 0857 QString DocumentChecker::fixLumaPath(const QString &file) 0858 { 0859 QDir searchPath(KdenliveSettings::mltpath()); 0860 QString fname = QFileInfo(file).fileName(); 0861 if (file.contains(QStringLiteral("PAL"))) { 0862 searchPath.cd(QStringLiteral("../lumas/PAL")); 0863 } else { 0864 searchPath.cd(QStringLiteral("../lumas/NTSC")); 0865 } 0866 QFileInfo result(searchPath, fname); 0867 if (result.exists()) { 0868 return result.filePath(); 0869 } 0870 // try to find luma in application path 0871 searchPath.setPath(QCoreApplication::applicationDirPath()); 0872 #ifdef Q_OS_WIN 0873 searchPath.cd(QStringLiteral("data/")); 0874 #else 0875 searchPath.cd(QStringLiteral("../share/kdenlive/")); 0876 #endif 0877 if (file.contains(QStringLiteral("/PAL"))) { 0878 searchPath.cd(QStringLiteral("lumas/PAL/")); 0879 } else { 0880 searchPath.cd(QStringLiteral("lumas/HD/")); 0881 } 0882 result.setFile(searchPath, fname); 0883 if (result.exists()) { 0884 return result.filePath(); 0885 } 0886 // Try in Kdenlive's standard KDE path 0887 QStringList resList = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, "lumas", QStandardPaths::LocateDirectory); 0888 for (auto res : resList) { 0889 if (!res.isEmpty()) { 0890 searchPath.setPath(res); 0891 if (file.contains(QStringLiteral("/PAL"))) { 0892 searchPath.cd(QStringLiteral("PAL")); 0893 } else { 0894 searchPath.cd(QStringLiteral("HD")); 0895 } 0896 result.setFile(searchPath, fname); 0897 if (result.exists()) { 0898 return result.filePath(); 0899 } 0900 } 0901 } 0902 return QString(); 0903 } 0904 0905 QString DocumentChecker::searchLuma(const QDir &dir, const QString &file) 0906 { 0907 // Try in user's chosen folder 0908 QString result = fixLumaPath(file); 0909 return result.isEmpty() ? searchPathRecursively(dir, QFileInfo(file).fileName()) : result; 0910 } 0911 0912 QString DocumentChecker::searchPathRecursively(const QDir &dir, const QString &fileName, ClipType::ProducerType type) 0913 { 0914 QString foundFileName; 0915 bool patternSlideshow = true; 0916 QDir searchDir(dir); 0917 QStringList filesAndDirs; 0918 qApp->processEvents(); 0919 /*if (m_abortSearch) { 0920 return QString(); 0921 }*/ 0922 if (type == ClipType::SlideShow) { 0923 if (fileName.contains(QLatin1Char('%'))) { 0924 searchDir.setNameFilters({fileName.section(QLatin1Char('%'), 0, -2) + QLatin1Char('*')}); 0925 filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable); 0926 0927 } else { 0928 patternSlideshow = false; 0929 QString slideDirName = QFileInfo(fileName).dir().dirName(); 0930 searchDir.setNameFilters({slideDirName}); 0931 filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable); 0932 } 0933 } else { 0934 searchDir.setNameFilters({fileName}); 0935 filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable); 0936 } 0937 if (!filesAndDirs.isEmpty()) { 0938 // File Found 0939 if (type == ClipType::SlideShow) { 0940 if (patternSlideshow) { 0941 return searchDir.absoluteFilePath(fileName); 0942 } else { 0943 // mime type slideshow 0944 searchDir.cd(filesAndDirs.first()); 0945 return searchDir.absoluteFilePath(QFileInfo(fileName).fileName()); 0946 } 0947 } else { 0948 return searchDir.absoluteFilePath(filesAndDirs.first()); 0949 } 0950 } 0951 searchDir.setNameFilters(QStringList()); 0952 filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); 0953 for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { 0954 foundFileName = searchPathRecursively(searchDir.absoluteFilePath(filesAndDirs.at(i)), fileName, type); 0955 if (!foundFileName.isEmpty()) { 0956 break; 0957 } 0958 } 0959 return foundFileName; 0960 } 0961 0962 QString DocumentChecker::searchDirRecursively(const QDir &dir, const QString &matchHash, const QString &fullName) 0963 { 0964 qApp->processEvents(); 0965 /*if (m_abortSearch) { 0966 return QString(); 0967 }*/ 0968 // Q_EMIT showScanning(i18n("Scanning %1", dir.absolutePath())); 0969 QString fileName = QFileInfo(fullName).fileName(); 0970 // Check main dir 0971 QString fileHash = ProjectClip::getFolderHash(dir, fileName).toHex(); 0972 if (fileHash == matchHash) { 0973 return dir.absoluteFilePath(fileName); 0974 } 0975 // Search subfolders 0976 const QStringList subDirs = dir.entryList(QDir::AllDirs | QDir::NoDot | QDir::NoDotDot); 0977 for (const QString &sub : subDirs) { 0978 QDir subFolder(dir.absoluteFilePath(sub)); 0979 fileHash = ProjectClip::getFolderHash(subFolder, fileName).toHex(); 0980 if (fileHash == matchHash) { 0981 return subFolder.absoluteFilePath(fileName); 0982 } 0983 } 0984 /*if (m_abortSearch) { 0985 return QString(); 0986 }*/ 0987 // Search inside subfolders 0988 for (const QString &sub : subDirs) { 0989 QDir subFolder(dir.absoluteFilePath(sub)); 0990 const QStringList subSubDirs = subFolder.entryList(QDir::AllDirs | QDir::NoDot | QDir::NoDotDot); 0991 for (const QString &subsub : subSubDirs) { 0992 QDir subDir(subFolder.absoluteFilePath(subsub)); 0993 QString result = searchDirRecursively(subDir, matchHash, fullName); 0994 if (!result.isEmpty()) { 0995 return result; 0996 } 0997 } 0998 } 0999 return QString(); 1000 } 1001 1002 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash, const QString &fileName) 1003 { 1004 if (matchSize.isEmpty() && matchHash.isEmpty()) { 1005 return searchPathRecursively(dir, QUrl::fromLocalFile(fileName).fileName()); 1006 } 1007 QString foundFileName; 1008 QByteArray fileData; 1009 QByteArray fileHash; 1010 QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable); 1011 for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { 1012 qApp->processEvents(); 1013 /*if (m_abortSearch) { 1014 return QString(); 1015 }*/ 1016 QFile file(dir.absoluteFilePath(filesAndDirs.at(i))); 1017 if (QString::number(file.size()) == matchSize) { 1018 if (file.open(QIODevice::ReadOnly)) { 1019 /* 1020 * 1 MB = 1 second per 450 files (or faster) 1021 * 10 MB = 9 seconds per 450 files (or faster) 1022 */ 1023 if (file.size() > 1000000 * 2) { 1024 fileData = file.read(1000000); 1025 if (file.seek(file.size() - 1000000)) { 1026 fileData.append(file.readAll()); 1027 } 1028 } else { 1029 fileData = file.readAll(); 1030 } 1031 file.close(); 1032 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); 1033 if (QString::fromLatin1(fileHash.toHex()) == matchHash) { 1034 return file.fileName(); 1035 } 1036 } 1037 } 1038 ////qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << file.size() << fileHash.toHex(); 1039 } 1040 filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); 1041 for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { 1042 foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash, fileName); 1043 if (!foundFileName.isEmpty()) { 1044 break; 1045 } 1046 } 1047 return foundFileName; 1048 } 1049 1050 QString DocumentChecker::ensureAbsolutePath(QString filepath) 1051 { 1052 if (!filepath.isEmpty() && QFileInfo(filepath).isRelative()) { 1053 filepath.prepend(m_root); 1054 } 1055 return filepath; 1056 } 1057 1058 QStringList DocumentChecker::getAssetsFilesByMltTag(const QDomDocument &doc, const QString &tagName, const QMap<QString, QString> &searchPairs) 1059 { 1060 QStringList files; 1061 1062 QDomNodeList assets = doc.elementsByTagName(tagName); 1063 int max = assets.count(); 1064 for (int i = 0; i < max; ++i) { 1065 QDomElement asset = assets.at(i).toElement(); 1066 const QString service = Xml::getXmlProperty(asset, QStringLiteral("mlt_service")); 1067 if (searchPairs.contains(service)) { 1068 const QString filepath = Xml::getXmlProperty(asset, searchPairs.value(service)); 1069 if (!filepath.isEmpty()) { 1070 files << filepath; 1071 } 1072 } 1073 } 1074 files.removeDuplicates(); 1075 return files; 1076 } 1077 1078 QStringList DocumentChecker::getAssetsServiceIds(const QDomDocument &doc, const QString &tagName) 1079 { 1080 QDomNodeList filters = doc.elementsByTagName(tagName); 1081 int max = filters.count(); 1082 QStringList services; 1083 for (int i = 0; i < max; ++i) { 1084 QDomElement filter = filters.at(i).toElement(); 1085 QString service = Xml::getXmlProperty(filter, QStringLiteral("kdenlive_id")); 1086 if (service.isEmpty()) { 1087 service = Xml::getXmlProperty(filter, QStringLiteral("mlt_service")); 1088 } 1089 services << service; 1090 } 1091 services.removeDuplicates(); 1092 return services; 1093 } 1094 1095 bool DocumentChecker::isMltBuildInLuma(const QString &lumaName) 1096 { 1097 // Since version 7 MLT contains built-in lumas named luma01.pgm to luma22.pgm 1098 static const QRegularExpression regex(QRegularExpression::anchoredPattern(R"(luma([0-9]{2})\.pgm)")); 1099 QRegularExpressionMatch match = regex.match(lumaName); 1100 if (match.hasMatch() && match.captured(1).toInt() > 0 && match.captured(1).toInt() < 23) { 1101 return true; 1102 } 1103 return false; 1104 } 1105 1106 // TODO remove? 1107 void DocumentChecker::fixMissingSource(const QString &id, const QDomNodeList &producers, const QDomNodeList &chains) 1108 { 1109 QDomElement e; 1110 for (int i = 0; i < producers.count(); ++i) { 1111 e = producers.item(i).toElement(); 1112 QString parentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); 1113 if (parentId == id) { 1114 // Fix clip 1115 e.removeAttribute(QStringLiteral("_missingsource")); 1116 } 1117 } 1118 for (int i = 0; i < chains.count(); ++i) { 1119 e = chains.item(i).toElement(); 1120 QString parentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); 1121 if (parentId == id) { 1122 // Fix clip 1123 e.removeAttribute(QStringLiteral("_missingsource")); 1124 } 1125 } 1126 } 1127 1128 QStringList DocumentChecker::fixSequences(QDomElement &e, const QDomNodeList &producers, const QStringList &tractorIds) 1129 { 1130 QStringList fixedSequences; 1131 QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); 1132 bool isBinClip = m_binIds.contains(e.attribute(QLatin1String("id"))); 1133 QString resource = Xml::getXmlProperty(e, QStringLiteral("resource")); 1134 1135 if (!(isBinClip && service == QLatin1String("tractor") && resource.endsWith(QLatin1String("tractor>")))) { 1136 // This is not a broken sequence clip (bug in Kdenlive 23.04.0) 1137 // nothing to fix, quit 1138 return {}; 1139 } 1140 1141 const QString brokenId = e.attribute(QStringLiteral("id")); 1142 const QString brokenUuid = Xml::getXmlProperty(e, QStringLiteral("kdenlive:uuid")); 1143 // Check that we have the original clip somewhere in the producers list 1144 if (brokenId != brokenUuid && tractorIds.contains(brokenUuid)) { 1145 // Replace bin clip entry 1146 for (int i = 0; i < m_binEntries.count(); ++i) { 1147 QDomElement e = m_binEntries.item(i).toElement(); 1148 if (e.attribute(QStringLiteral("producer")) == brokenId) { 1149 // Match 1150 e.setAttribute(QStringLiteral("producer"), brokenUuid); 1151 fixedSequences.append(brokenId); 1152 return fixedSequences; 1153 } 1154 } 1155 } else { 1156 // entry not found, this is a more complex recovery: 1157 // 1. Change tag to tractor 1158 // 2. Reinsert all tractor as tracks 1159 // 3. Move the node just before main_bin to ensure its children tracks are defined before it 1160 // e.setTagName(QStringLiteral("tractor")); 1161 // 4. Xml::removeXmlProperty(e, QStringLiteral("resource")); 1162 1163 if (!e.elementsByTagName(QStringLiteral("track")).isEmpty()) { 1164 return {}; 1165 } 1166 1167 // Change tag, add tracks and move to the end of the document (just before the main_bin) 1168 QDomNodeList tracks = m_doc.elementsByTagName(QStringLiteral("track")); 1169 1170 QStringList insertedTractors; 1171 for (int k = 0; k < tracks.count(); ++k) { 1172 // Collect names of already inserted tractors / playlists 1173 QDomElement prod = tracks.item(k).toElement(); 1174 insertedTractors << prod.attribute(QStringLiteral("producer")); 1175 } 1176 // Tracks must be inserted before transitions / filters 1177 QDomNode lastProperty = e.lastChildElement(QStringLiteral("property")); 1178 if (lastProperty.isNull()) { 1179 lastProperty = e.firstChildElement(); 1180 } 1181 // Find black producer id 1182 for (int k = 0; k < producers.count(); ++k) { 1183 QDomElement prod = producers.item(k).toElement(); 1184 if (Xml::hasXmlProperty(prod, QStringLiteral("kdenlive:playlistid"))) { 1185 // Match, we found black track producer 1186 QDomElement tk = m_doc.createElement(QStringLiteral("track")); 1187 tk.setAttribute(QStringLiteral("producer"), prod.attribute(QStringLiteral("id"))); 1188 lastProperty = e.insertAfter(tk, lastProperty); 1189 break; 1190 } 1191 } 1192 // Insert real tracks 1193 QDomNodeList tractors = m_doc.elementsByTagName(QStringLiteral("tractor")); 1194 for (int j = 0; j < tractors.count(); ++j) { 1195 QDomElement current = tractors.item(j).toElement(); 1196 // Check all non used tractors and attach them as tracks 1197 if (!Xml::hasXmlProperty(current, QStringLiteral("kdenlive:projectTractor")) && !insertedTractors.contains(current.attribute("id"))) { 1198 QDomElement tk = m_doc.createElement(QStringLiteral("track")); 1199 tk.setAttribute(QStringLiteral("producer"), current.attribute(QStringLiteral("id"))); 1200 lastProperty = e.insertAfter(tk, lastProperty); 1201 } 1202 } 1203 1204 QDomNode brokenSequence = m_doc.documentElement().removeChild(e); 1205 QDomElement fixedSequence = brokenSequence.toElement(); 1206 fixedSequence.setTagName(QStringLiteral("tractor")); 1207 Xml::removeXmlProperty(fixedSequence, QStringLiteral("resource")); 1208 Xml::removeXmlProperty(fixedSequence, QStringLiteral("mlt_service")); 1209 1210 QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); 1211 for (int p = 0; p < playlists.count(); ++p) { 1212 if (playlists.at(p).toElement().attribute(QStringLiteral("id")) == BinPlaylist::binPlaylistId) { 1213 QDomNode mainBinPlaylist = playlists.at(p); 1214 m_doc.documentElement().insertBefore(brokenSequence, mainBinPlaylist); 1215 } 1216 } 1217 1218 fixedSequences.append(brokenId); 1219 return fixedSequences; 1220 } 1221 return fixedSequences; 1222 } 1223 1224 void DocumentChecker::fixProxyClip(const QDomNodeList &items, const QString &id, const QString &oldUrl, const QString &newUrl) 1225 { 1226 QDomElement e; 1227 for (int i = 0; i < items.count(); ++i) { 1228 e = items.item(i).toElement(); 1229 QString parentId = getKdenliveClipId(e); 1230 if (parentId != id) { 1231 continue; 1232 } 1233 // Fix clip 1234 QString resource = Xml::getXmlProperty(e, QStringLiteral("resource")); 1235 bool timewarp = false; 1236 if (Xml::getXmlProperty(e, QStringLiteral("mlt_service")) == QLatin1String("timewarp")) { 1237 timewarp = true; 1238 resource = Xml::getXmlProperty(e, QStringLiteral("warp_resource")); 1239 } 1240 if (resource == oldUrl) { 1241 if (timewarp) { 1242 Xml::setXmlProperty(e, QStringLiteral("resource"), Xml::getXmlProperty(e, QStringLiteral("warp_speed")) + ":" + newUrl); 1243 Xml::setXmlProperty(e, QStringLiteral("warp_resource"), newUrl); 1244 } else { 1245 Xml::setXmlProperty(e, QStringLiteral("resource"), newUrl); 1246 } 1247 } 1248 if (!Xml::getXmlProperty(e, QStringLiteral("kdenlive:proxy")).isEmpty()) { 1249 // Only set originalurl on master producer 1250 Xml::setXmlProperty(e, QStringLiteral("kdenlive:proxy"), newUrl); 1251 } 1252 } 1253 } 1254 1255 void DocumentChecker::fixTitleImage(QDomElement &e, const QString &oldPath, const QString &newPath) 1256 { 1257 QDomNodeList properties = e.childNodes(); 1258 QDomElement property; 1259 for (int j = 0; j < properties.count(); ++j) { 1260 property = properties.item(j).toElement(); 1261 if (property.attribute(QStringLiteral("name")) == QLatin1String("xmldata")) { 1262 QString xml = property.firstChild().nodeValue(); 1263 xml.replace(oldPath, newPath); 1264 property.firstChild().setNodeValue(xml); 1265 break; 1266 } 1267 } 1268 } 1269 1270 void DocumentChecker::fixTitleFont(const QDomNodeList &producers, const QString &oldFont, const QString &newFont) 1271 { 1272 QDomElement e; 1273 for (int i = 0; i < producers.count(); ++i) { 1274 e = producers.item(i).toElement(); 1275 QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); 1276 // Fix clip 1277 if (service == QLatin1String("kdenlivetitle")) { 1278 QString xml = Xml::getXmlProperty(e, QStringLiteral("xmldata")); 1279 QStringList fonts = TitleWidget::extractFontList(xml); 1280 if (fonts.contains(oldFont)) { 1281 xml.replace(QString("font=\"%1\"").arg(oldFont), QString("font=\"%1\"").arg(newFont)); 1282 Xml::setXmlProperty(e, QStringLiteral("xmldata"), xml); 1283 Xml::setXmlProperty(e, QStringLiteral("force_reload"), QStringLiteral("2")); 1284 Xml::setXmlProperty(e, QStringLiteral("_fullreload"), QStringLiteral("2")); 1285 } 1286 } 1287 } 1288 } 1289 1290 void DocumentChecker::fixAssetResource(const QDomNodeList &assets, const QMap<QString, QString> &searchPairs, const QString &oldPath, const QString &newPath) 1291 { 1292 for (int i = 0; i < assets.count(); ++i) { 1293 QDomElement asset = assets.at(i).toElement(); 1294 1295 QString service = Xml::getXmlProperty(asset, QStringLiteral("mlt_service")); 1296 if (searchPairs.contains(service)) { 1297 QString currentPath = Xml::getXmlProperty(asset, searchPairs.value(service)); 1298 if (!currentPath.isEmpty() && ensureAbsolutePath(currentPath) == oldPath) { 1299 Xml::setXmlProperty(asset, searchPairs.value(service), newPath); 1300 } 1301 } 1302 } 1303 } 1304 1305 void DocumentChecker::usePlaceholderForClip(const QDomNodeList &items, const QString &clipId) 1306 { 1307 // items: chains or producers 1308 1309 QDomElement e; 1310 for (int i = items.count() - 1; i >= 0; --i) { 1311 // Setting the tag name (see below) might change it and this will remove the item from the original list, so we need to parse in reverse order 1312 e = items.item(i).toElement(); 1313 if (Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")) == clipId) { 1314 // Fix clip 1315 Xml::setXmlProperty(e, QStringLiteral("_placeholder"), QStringLiteral("1")); 1316 Xml::setXmlProperty(e, QStringLiteral("kdenlive:orig_service"), Xml::getXmlProperty(e, QStringLiteral("mlt_service"))); 1317 1318 // In MLT 7.14/15, link_swresample crashes on invalid avformat clips, 1319 // so switch to producer instead of chain to use filter_swresample. 1320 // If we have an producer already, it obviously makes no difference. 1321 e.setTagName(QStringLiteral("producer")); 1322 } 1323 } 1324 } 1325 1326 void DocumentChecker::removeAssetsById(QDomDocument &doc, const QString &tagName, const QStringList &idsToDelete) 1327 { 1328 if (idsToDelete.isEmpty()) { 1329 return; 1330 } 1331 1332 QDomNodeList assets = doc.elementsByTagName(tagName); 1333 for (int i = 0; i < assets.count(); ++i) { 1334 QDomElement asset = assets.item(i).toElement(); 1335 QString service = Xml::getXmlProperty(asset, QStringLiteral("kdenlive_id")); 1336 if (service.isEmpty()) { 1337 service = Xml::getXmlProperty(asset, QStringLiteral("mlt_service")); 1338 } 1339 if (idsToDelete.contains(service)) { 1340 // Remove asset 1341 asset.parentNode().removeChild(asset); 1342 --i; 1343 } 1344 } 1345 } 1346 1347 void DocumentChecker::fixAssetsById(QDomDocument &doc, const QString &tagName, const QString &oldId, const QString &newId) 1348 { 1349 QDomNodeList assets = doc.elementsByTagName(tagName); 1350 for (int i = 0; i < assets.count(); ++i) { 1351 QDomElement asset = assets.item(i).toElement(); 1352 QString service = Xml::getXmlProperty(asset, QStringLiteral("kdenlive_id")); 1353 if (service.isEmpty()) { 1354 service = Xml::getXmlProperty(asset, QStringLiteral("mlt_service")); 1355 } 1356 if (service == oldId) { 1357 // Rename asset 1358 Xml::setXmlProperty(asset, QStringLiteral("kdenlive_id"), newId); 1359 Xml::setXmlProperty(asset, QStringLiteral("mlt_service"), newId); 1360 } 1361 } 1362 } 1363 1364 void DocumentChecker::fixClip(const QDomNodeList &items, const QString &clipId, const QString &newPath) 1365 { 1366 QDomElement e; 1367 // Changing the tag name (below) will remove the producer from the list, so we need to parse in reverse order 1368 for (int i = items.count() - 1; i >= 0; --i) { 1369 e = items.item(i).toElement(); 1370 if (getKdenliveClipId(e) != clipId) { 1371 continue; 1372 } 1373 1374 QString service = Xml::getXmlProperty(e, QStringLiteral("mlt_service")); 1375 QString updatedPath = newPath; 1376 1377 if (Xml::hasXmlProperty(e, QStringLiteral("kdenlive:originalurl"))) { 1378 // Only set originalurl on master producer 1379 Xml::setXmlProperty(e, QStringLiteral("kdenlive:originalurl"), newPath); 1380 } 1381 if (Xml::hasXmlProperty(e, QStringLiteral("kdenlive:original.resource"))) { 1382 // Only set original.resource on master producer 1383 Xml::setXmlProperty(e, QStringLiteral("kdenlive:original.resource"), newPath); 1384 } 1385 if (service == QLatin1String("timewarp")) { 1386 Xml::setXmlProperty(e, QStringLiteral("warp_resource"), updatedPath); 1387 updatedPath.prepend(Xml::getXmlProperty(e, QStringLiteral("warp_speed")) + QLatin1Char(':')); 1388 } 1389 if (service.startsWith(QLatin1String("avformat")) && e.tagName() == QLatin1String("producer")) { 1390 e.setTagName(QStringLiteral("chain")); 1391 } 1392 if (Xml::hasXmlProperty(e, QStringLiteral("text"))) { 1393 if (Xml::getXmlProperty(e, QStringLiteral("text")) == QLatin1String("INVALID") && service == QLatin1String("qimage")) { 1394 // Clip was previously opened as placeholder, remove the extra stuff 1395 Xml::removeXmlProperty(e, QStringLiteral("text")); 1396 Xml::removeXmlProperty(e, QStringLiteral("fgcolour")); 1397 Xml::removeXmlProperty(e, QStringLiteral("bgcolour")); 1398 Xml::removeXmlProperty(e, QStringLiteral("olcolour")); 1399 Xml::removeXmlProperty(e, QStringLiteral("outline")); 1400 Xml::removeXmlProperty(e, QStringLiteral("align")); 1401 Xml::removeXmlProperty(e, QStringLiteral("pad")); 1402 Xml::removeXmlProperty(e, QStringLiteral("family")); 1403 Xml::removeXmlProperty(e, QStringLiteral("size")); 1404 Xml::removeXmlProperty(e, QStringLiteral("style")); 1405 Xml::removeXmlProperty(e, QStringLiteral("weight")); 1406 Xml::removeXmlProperty(e, QStringLiteral("encoding")); 1407 // meta.media size was set to the size of the "INVALID" text, not to the original image, so remove 1408 Xml::removeXmlProperty(e, QStringLiteral("meta.media.width")); 1409 Xml::removeXmlProperty(e, QStringLiteral("meta.media.height")); 1410 } 1411 } 1412 1413 Xml::setXmlProperty(e, QStringLiteral("resource"), updatedPath); 1414 } 1415 } 1416 1417 void DocumentChecker::removeClip(const QDomNodeList &producers, const QDomNodeList &chains, const QDomNodeList &playlists, const QString &clipId) 1418 { 1419 QDomElement e; 1420 // remove the clips producer 1421 for (int i = 0; i < producers.count(); ++i) { 1422 e = producers.item(i).toElement(); 1423 if (Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")) == clipId) { 1424 // Mark clip for deletion 1425 Xml::setXmlProperty(e, QStringLiteral("kdenlive:remove"), QStringLiteral("1")); 1426 } 1427 } 1428 1429 for (int i = 0; i < chains.count(); ++i) { 1430 e = chains.item(i).toElement(); 1431 if (Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")) == clipId) { 1432 // Mark clip for deletion 1433 Xml::setXmlProperty(e, QStringLiteral("kdenlive:remove"), QStringLiteral("1")); 1434 } 1435 } 1436 1437 // also remove all instances of the clip in playlists 1438 for (int i = 0; i < playlists.count(); ++i) { 1439 QDomNodeList entries = playlists.at(i).toElement().elementsByTagName(QStringLiteral("entry")); 1440 for (int j = 0; j < entries.count(); ++j) { 1441 e = entries.item(j).toElement(); 1442 if (Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")) == clipId) { 1443 // Mark clip for deletion 1444 Xml::setXmlProperty(e, QStringLiteral("kdenlive:remove"), QStringLiteral("1")); 1445 } 1446 } 1447 } 1448 } 1449 1450 void DocumentChecker::fixMissingItem(const DocumentChecker::DocumentResource &resource, const QDomNodeList &producers, const QDomNodeList &chains, 1451 const QDomNodeList &trans, const QDomNodeList &filters) 1452 { 1453 qDebug() << "==== FIXING PRODUCER WITH ID: " << resource.clipId; 1454 1455 /*if (resource.type == MissingType::Sequence) { 1456 // Already processed 1457 return; 1458 }*/ 1459 1460 QDomElement e; 1461 if (resource.type == MissingType::TitleImage) { 1462 // Title clips are not embedded in chains 1463 // edit images embedded in titles 1464 for (int i = 0; i < producers.count(); ++i) { 1465 e = producers.item(i).toElement(); 1466 QString parentId = getKdenliveClipId(e); 1467 if (parentId == resource.clipId) { 1468 fixTitleImage(e, resource.originalFilePath, resource.newFilePath); 1469 } 1470 } 1471 } else if (resource.type == MissingType::Clip) { 1472 if (resource.status == MissingStatus::Fixed) { 1473 // edit clip url 1474 fixClip(chains, resource.clipId, resource.newFilePath); 1475 fixClip(producers, resource.clipId, resource.newFilePath); 1476 } else if (resource.status == MissingStatus::Remove) { 1477 QDomNodeList playlists = m_doc.elementsByTagName(QStringLiteral("playlist")); 1478 removeClip(producers, chains, playlists, resource.clipId); 1479 } else if (resource.status == MissingStatus::Placeholder /*child->data(0, statusRole).toInt() == CLIPPLACEHOLDER*/) { 1480 usePlaceholderForClip(producers, resource.clipId); 1481 usePlaceholderForClip(chains, resource.clipId); 1482 } 1483 } else if (resource.type == MissingType::Proxy) { 1484 if (resource.status == MissingStatus::Fixed) { 1485 fixProxyClip(producers, resource.clipId, resource.originalFilePath, resource.newFilePath); 1486 fixProxyClip(chains, resource.clipId, resource.originalFilePath, resource.newFilePath); 1487 } else if (resource.status == MissingStatus::Reload) { 1488 removeProxy(producers, resource.clipId, true); 1489 removeProxy(chains, resource.clipId, true); 1490 } else if (resource.status == MissingStatus::Remove) { 1491 removeProxy(producers, resource.clipId, false); 1492 removeProxy(chains, resource.clipId, false); 1493 } 1494 1495 } else if (resource.type == MissingType::TitleFont) { 1496 // Parse all title producers 1497 fixTitleFont(producers, resource.originalFilePath, resource.newFilePath); 1498 } else if (resource.type == MissingType::Luma) { 1499 QString newPath = resource.newFilePath; 1500 if (resource.status == MissingStatus::Remove) { 1501 newPath.clear(); 1502 } 1503 fixAssetResource(trans, getLumaPairs(), resource.originalFilePath, newPath); 1504 } else if (resource.type == MissingType::AssetFile) { 1505 QString newPath = resource.newFilePath; 1506 if (resource.status == MissingStatus::Remove) { 1507 newPath.clear(); 1508 } 1509 fixAssetResource(filters, getAssetPairs(), resource.originalFilePath, newPath); 1510 } else if (resource.type == MissingType::Effect) { 1511 if (resource.status == MissingStatus::Fixed) { 1512 fixAssetsById(m_doc, QStringLiteral("filter"), resource.originalFilePath, resource.newFilePath); 1513 } else if (resource.status == MissingStatus::Remove) { 1514 removeAssetsById(m_doc, QStringLiteral("filter"), {resource.originalFilePath}); 1515 } 1516 } else if (resource.type == MissingType::Transition && resource.status == MissingStatus::Remove) { 1517 removeAssetsById(m_doc, QStringLiteral("transition"), {resource.originalFilePath}); 1518 } 1519 } 1520 1521 ClipType::ProducerType DocumentChecker::getClipType(const QString &service, const QString &resource) 1522 { 1523 ClipType::ProducerType type = ClipType::Unknown; 1524 if (service.startsWith(QLatin1String("avformat")) || service == QLatin1String("framebuffer") || service == QLatin1String("timewarp")) { 1525 type = ClipType::AV; 1526 } else if (service == QLatin1String("qimage") || service == QLatin1String("pixbuf")) { 1527 bool slideshow = isSlideshow(resource); 1528 if (slideshow) { 1529 type = ClipType::SlideShow; 1530 } else { 1531 type = ClipType::Image; 1532 } 1533 } else if (service == QLatin1String("mlt") || service == QLatin1String("xml")) { 1534 type = ClipType::Playlist; 1535 } 1536 return type; 1537 } 1538 1539 QString DocumentChecker::getProducerResource(const QDomElement &producer) 1540 { 1541 QString service = Xml::getXmlProperty(producer, QStringLiteral("mlt_service")); 1542 QString resource = Xml::getXmlProperty(producer, QStringLiteral("resource")); 1543 if (resource.isEmpty()) { 1544 return QString(); 1545 } 1546 if (service == QLatin1String("timewarp")) { 1547 // slowmotion clip, trim speed info 1548 resource = Xml::getXmlProperty(producer, QStringLiteral("warp_resource")); 1549 } else if (service == QLatin1String("framebuffer")) { 1550 // slowmotion clip, trim speed info 1551 resource = resource.section(QLatin1Char('?'), 0, 0); 1552 } 1553 return ensureAbsolutePath(resource); 1554 } 1555 1556 QString DocumentChecker::getKdenliveClipId(const QDomElement &producer) 1557 { 1558 QString clipId = Xml::getXmlProperty(producer, QStringLiteral("kdenlive:id")); 1559 if (clipId.isEmpty()) { 1560 // Older project file format 1561 clipId = producer.attribute(QStringLiteral("id")).section(QLatin1Char('_'), 0, 0); 1562 } 1563 return clipId; 1564 } 1565 1566 QString DocumentChecker::readableNameForClipType(ClipType::ProducerType type) 1567 { 1568 switch (type) { 1569 case ClipType::AV: 1570 return i18n("Video clip"); 1571 case ClipType::SlideShow: 1572 return i18n("Slideshow clip"); 1573 case ClipType::Image: 1574 return i18n("Image clip"); 1575 case ClipType::Playlist: 1576 return i18n("Playlist clip"); 1577 case ClipType::Text: 1578 return i18n("Title Image"); // ? 1579 case ClipType::Unknown: 1580 return i18n("Unknown"); 1581 default: 1582 return {}; 1583 } 1584 } 1585 1586 QString DocumentChecker::readableNameForMissingType(MissingType type) 1587 { 1588 switch (type) { 1589 case MissingType::Clip: 1590 return i18n("Clip"); 1591 case MissingType::TitleFont: 1592 return i18n("Title Font"); 1593 case MissingType::TitleImage: 1594 return i18n("Title Image"); 1595 case MissingType::Luma: 1596 return i18n("Luma file"); 1597 case MissingType::AssetFile: 1598 return i18n("Asset file"); 1599 case MissingType::Proxy: 1600 return i18n("Proxy clip"); 1601 case MissingType::Effect: 1602 return i18n("Effect"); 1603 case MissingType::Transition: 1604 return i18n("Transition"); 1605 case MissingType::CircularRef: 1606 return i18n("Corrupted sequence"); 1607 default: 1608 return i18n("Unknown"); 1609 } 1610 } 1611 1612 QString DocumentChecker::readableNameForMissingStatus(MissingStatus type) 1613 { 1614 switch (type) { 1615 case MissingStatus::Fixed: 1616 return i18n("Fixed"); 1617 case MissingStatus::Reload: 1618 return i18n("Reload"); 1619 case MissingStatus::Missing: 1620 return i18n("Missing"); 1621 case MissingStatus::MissingButProxy: 1622 return i18n("Missing, but proxy available"); 1623 case MissingStatus::Placeholder: 1624 return i18n("Placeholder"); 1625 case MissingStatus::Remove: 1626 return i18n("Remove"); 1627 default: 1628 return i18n("Unknown"); 1629 } 1630 } 1631 1632 // TODO: remove? 1633 QStringList DocumentChecker::getInfoMessages() 1634 { 1635 QStringList messages; 1636 if (itemsContain(MissingType::Luma) || itemsContain(MissingType::AssetFile) || itemsContain(MissingType::Clip)) { 1637 messages.append(i18n("The project file contains missing clips or files.")); 1638 } 1639 if (itemsContain(MissingType::Proxy)) { 1640 messages.append(i18n("Missing proxies can be recreated on opening.")); 1641 } 1642 // TODO 1643 /*if (!m_missingSources.isEmpty()) { 1644 messages.append(i18np("The project file contains a missing clip, you can still work with its proxy.", 1645 "The project file contains %1 missing clips, you can still work with their proxies.", m_missingSources.count())); 1646 } 1647 if (!m_changedClips.isEmpty()) { 1648 messages.append(i18np("The project file contains one modified clip, it will be reloaded.", 1649 "The project file contains %1 modified clips, they will be reloaded.", m_changedClips.count())); 1650 }*/ 1651 return messages; 1652 } 1653 1654 bool DocumentChecker::itemsContain(MissingType type, const QString &path, MissingStatus status) 1655 { 1656 for (auto item : m_items) { 1657 if (item.status != status) { 1658 continue; 1659 } 1660 if (type != item.type) { 1661 continue; 1662 } 1663 if (item.originalFilePath == path || path.isEmpty()) { 1664 return true; 1665 } 1666 } 1667 return false; 1668 } 1669 1670 int DocumentChecker::itemIndexByClipId(const QString &clipId) 1671 { 1672 for (std::size_t i = 0; i < m_items.size(); i++) { 1673 if (m_items[i].clipId == clipId) { 1674 return i; 1675 } 1676 } 1677 return -1; 1678 } 1679 1680 bool DocumentChecker::isSlideshow(const QString &resource) 1681 { 1682 return resource.contains(QStringLiteral("/.all.")) || resource.contains(QStringLiteral("\\.all.")) || resource.contains(QLatin1Char('?')) || 1683 resource.contains(QLatin1Char('%')); 1684 } 1685 1686 bool DocumentChecker::isProfileHD(const QDomDocument &doc) 1687 { 1688 QDomElement profile = doc.documentElement().firstChildElement(QStringLiteral("profile")); 1689 if (!profile.isNull()) { 1690 if (profile.attribute(QStringLiteral("width")).toInt() < 1000) { 1691 return false; 1692 } 1693 } 1694 return true; 1695 } 1696 1697 bool DocumentChecker::isSequenceWithSpeedEffect(const QDomElement &producer) 1698 { 1699 QString service = Xml::getXmlProperty(producer, QStringLiteral("mlt_service")); 1700 QString resource = getProducerResource(producer); 1701 1702 bool isSequence = resource.endsWith(QLatin1String(".mlt")) && resource.contains(QLatin1String("/sequences/")); 1703 1704 QVector<QDomNode> links = Xml::getDirectChildrenByTagName(producer, QStringLiteral("link")); 1705 bool isTimeremap = service == QLatin1String("xml") && !links.isEmpty() && 1706 Xml::getXmlProperty(links.first().toElement(), QStringLiteral("mlt_service")) == QLatin1String("timeremap"); 1707 1708 return isSequence && (service == QLatin1String("timewarp") || isTimeremap); 1709 } 1710 1711 QMap<DocumentChecker::MissingType, int> DocumentChecker::getCheckResults() 1712 { 1713 QMap<DocumentChecker::MissingType, int> missingResults; 1714 for (auto item : m_items) { 1715 if (missingResults.contains(item.type)) { 1716 missingResults[item.type] = missingResults.value(item.type) + 1; 1717 } else { 1718 missingResults.insert(item.type, 1); 1719 } 1720 } 1721 return missingResults; 1722 }