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