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 }