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 }