File indexing completed on 2024-04-14 04:46:31

0001 /*
0002     SPDX-FileCopyrightText: 2007 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 
0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "kdenlivedoc.h"
0008 #include "bin/bin.h"
0009 #include "bin/bincommands.h"
0010 #include "bin/binplaylist.hpp"
0011 #include "bin/clipcreator.hpp"
0012 #include "bin/mediabrowser.h"
0013 #include "bin/model/markerlistmodel.hpp"
0014 #include "bin/model/markersortmodel.h"
0015 #include "bin/model/subtitlemodel.hpp"
0016 #include "bin/projectclip.h"
0017 #include "bin/projectitemmodel.h"
0018 #include "core.h"
0019 #include "dialogs/profilesdialog.h"
0020 #include "documentchecker.h"
0021 #include "documentvalidator.h"
0022 #include "docundostack.hpp"
0023 #include "effects/effectsrepository.hpp"
0024 #include "kdenlivesettings.h"
0025 #include "mainwindow.h"
0026 #include "mltcontroller/clipcontroller.h"
0027 #include "profiles/profilemodel.hpp"
0028 #include "profiles/profilerepository.hpp"
0029 #include "timeline2/model/builders/meltBuilder.hpp"
0030 #include "timeline2/model/timelineitemmodel.hpp"
0031 #include "titler/titlewidget.h"
0032 #include "transitions/transitionsrepository.hpp"
0033 #include <config-kdenlive.h>
0034 
0035 #include "utils/KMessageBox_KdenliveCompat.h"
0036 #include <KBookmark>
0037 #include <KBookmarkManager>
0038 #include <KIO/CopyJob>
0039 #include <KIO/FileCopyJob>
0040 #include <KJobWidgets>
0041 #include <KLocalizedString>
0042 #include <KMessageBox>
0043 
0044 #include "kdenlive_debug.h"
0045 #include <QCryptographicHash>
0046 #include <QDomImplementation>
0047 #include <QFile>
0048 #include <QFileDialog>
0049 #include <QJsonArray>
0050 #include <QJsonObject>
0051 #include <QSaveFile>
0052 #include <QStandardPaths>
0053 #include <QUndoGroup>
0054 #include <QUndoStack>
0055 #include <memory>
0056 #include <mlt++/Mlt.h>
0057 
0058 #include <audio/audioInfo.h>
0059 #include <locale>
0060 #ifdef Q_OS_MAC
0061 #include <xlocale.h>
0062 #endif
0063 
0064 // The document version is the Kdenlive project file version. Only increment this on major releases if
0065 // the file format changes and requires manual processing in the document validator.
0066 // Increasing the document version means that older Kdenlive versions won't be able to open the project files
0067 const double DOCUMENTVERSION = 1.1;
0068 
0069 // The index for all timeline objects
0070 int KdenliveDoc::next_id = 0;
0071 
0072 // create a new blank document
0073 KdenliveDoc::KdenliveDoc(QString projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap<QString, QString> &properties,
0074                          const QMap<QString, QString> &metadata, const std::pair<int, int> &tracks, int audioChannels, MainWindow *parent)
0075     : QObject(parent)
0076     , m_autosave(nullptr)
0077     , m_uuid(QUuid::createUuid())
0078     , m_clipsCount(0)
0079     , m_commandStack(std::make_shared<DocUndoStack>(undoGroup))
0080     , m_modified(false)
0081     , m_documentOpenStatus(CleanProject)
0082     , m_url(QUrl())
0083     , m_projectFolder(std::move(projectFolder))
0084 {
0085     next_id = 0;
0086     if (parent) {
0087         connect(this, &KdenliveDoc::updateCompositionMode, parent, &MainWindow::slotUpdateCompositeAction);
0088     }
0089     connect(m_commandStack.get(), &QUndoStack::indexChanged, this, &KdenliveDoc::slotModified);
0090     connect(m_commandStack.get(), &DocUndoStack::invalidate, this, &KdenliveDoc::checkPreviewStack, Qt::DirectConnection);
0091     // connect(m_commandStack, SIGNAL(cleanChanged(bool)), this, SLOT(setModified(bool)));
0092     pCore->taskManager.unBlock();
0093     initializeProperties(true, tracks, audioChannels);
0094 
0095     // Load properties
0096     QMapIterator<QString, QString> i(properties);
0097     while (i.hasNext()) {
0098         i.next();
0099         m_documentProperties[i.key()] = i.value();
0100     }
0101 
0102     // Load metadata
0103     QMapIterator<QString, QString> j(metadata);
0104     while (j.hasNext()) {
0105         j.next();
0106         m_documentMetadata[j.key()] = j.value();
0107     }
0108     pCore->setCurrentProfile(profileName);
0109     m_document = createEmptyDocument(tracks.first, tracks.second);
0110     updateProjectProfile(false);
0111     updateProjectFolderPlacesEntry();
0112     initCacheDirs();
0113 }
0114 
0115 KdenliveDoc::KdenliveDoc(const QUrl &url, QDomDocument &newDom, QString projectFolder, QUndoGroup *undoGroup, MainWindow *parent)
0116     : QObject(parent)
0117     , m_autosave(nullptr)
0118     , m_uuid(QUuid::createUuid())
0119     , m_document(newDom)
0120     , m_clipsCount(0)
0121     , m_commandStack(std::make_shared<DocUndoStack>(undoGroup))
0122     , m_modified(false)
0123     , m_documentOpenStatus(CleanProject)
0124     , m_url(url)
0125     , m_projectFolder(std::move(projectFolder))
0126 {
0127     next_id = 0;
0128     if (parent) {
0129         connect(this, &KdenliveDoc::updateCompositionMode, parent, &MainWindow::slotUpdateCompositeAction);
0130     }
0131     connect(m_commandStack.get(), &QUndoStack::indexChanged, this, &KdenliveDoc::slotModified);
0132     connect(m_commandStack.get(), &DocUndoStack::invalidate, this, &KdenliveDoc::checkPreviewStack, Qt::DirectConnection);
0133     pCore->taskManager.unBlock();
0134     initializeProperties(false);
0135     updateClipsCount();
0136 }
0137 
0138 KdenliveDoc::KdenliveDoc(std::shared_ptr<DocUndoStack> undoStack, std::pair<int, int> tracks, MainWindow *parent)
0139     : QObject(parent)
0140     , m_autosave(nullptr)
0141     , m_uuid(QUuid::createUuid())
0142     , m_clipsCount(0)
0143     , m_modified(false)
0144     , m_documentOpenStatus(CleanProject)
0145 {
0146     next_id = 0;
0147     m_commandStack = undoStack;
0148     m_document = createEmptyDocument(tracks.second, tracks.first);
0149     initializeProperties(true, tracks, 2);
0150     loadDocumentProperties();
0151     pCore->taskManager.unBlock();
0152 }
0153 
0154 DocOpenResult KdenliveDoc::Open(const QUrl &url, const QString &projectFolder, QUndoGroup *undoGroup,
0155     bool recoverCorruption, MainWindow *parent)
0156 {
0157 
0158     DocOpenResult result = DocOpenResult{};
0159 
0160     if (url.isEmpty() || !url.isValid()) {
0161         result.setError(i18n("Invalid file path"));
0162         return result;
0163     }
0164 
0165     qCDebug(KDENLIVE_LOG) << "// opening file " << url.toLocalFile();
0166 
0167     QFile file(url.toLocalFile());
0168     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0169         result.setError(i18n("Cannot open file %1", url.toLocalFile()));
0170         return result;
0171     }
0172 
0173     QDomDocument domDoc {};
0174     int line;
0175     int col;
0176     QString domErrorMessage;
0177 
0178 
0179     if (recoverCorruption) {
0180         // this seems to also drop valid non-BMP Unicode characters, so only do
0181         // it if the file is unreadable otherwise
0182         QDomImplementation::setInvalidDataPolicy(QDomImplementation::DropInvalidChars);
0183         result.setModified(true);
0184     }
0185     bool success = domDoc.setContent(&file, false, &domErrorMessage, &line, &col);
0186 
0187     if (!success) {
0188         if (recoverCorruption) {
0189             // Try to recover broken file produced by Kdenlive 0.9.4
0190             int correction = 0;
0191             QString playlist = QString::fromUtf8(file.readAll());
0192             while (!success && correction < 2) {
0193                 int errorPos = 0;
0194                 line--;
0195                 col = col - 2;
0196                 for (int k = 0; k < line && errorPos < playlist.length(); ++k) {
0197                     errorPos = playlist.indexOf(QLatin1Char('\n'), errorPos);
0198                     errorPos++;
0199                 }
0200                 errorPos += col;
0201                 if (errorPos >= playlist.length()) {
0202                     break;
0203                 }
0204                 playlist.remove(errorPos, 1);
0205                 line = 0;
0206                 col = 0;
0207                 success = domDoc.setContent(playlist, false, &domErrorMessage, &line, &col);
0208                 correction++;
0209             }
0210             if (!success) {
0211                 result.setError(i18n("Could not recover corrupted file."));
0212                 return result;
0213             } else {
0214                 qCDebug(KDENLIVE_LOG) << "Corrupted document read successfully.";
0215                 result.setModified(true);
0216             }
0217         } else {
0218             result.setError(i18n("Cannot open file %1:\n%2 (line %3, col %4)",
0219                 url.toLocalFile(), domErrorMessage, line, col));
0220             return result;
0221         }
0222     }
0223     file.close();
0224 
0225 
0226     qCDebug(KDENLIVE_LOG) << "// validating project file";
0227     DocumentValidator validator(domDoc, url);
0228     success = validator.isProject();
0229     if (!success) {
0230         // It is not a project file
0231         result.setError(i18n("File %1 is not a Kdenlive project file", url.toLocalFile()));
0232         return result;
0233     }
0234 
0235     auto validationResult = validator.validate(DOCUMENTVERSION);
0236     success = validationResult.first;
0237     if (!success) {
0238         result.setError(i18n("File %1 is not a valid Kdenlive project file.", url.toLocalFile()));
0239         return result;
0240     }
0241 
0242     if (!validationResult.second.isEmpty()) {
0243         qDebug() << "DECIMAL POINT has changed to . (was " << validationResult.second << "previously)";
0244         result.setModified(true);
0245     }
0246 
0247     if (!KdenliveSettings::gpu_accel()) {
0248         success = validator.checkMovit();
0249     }
0250     if (!success) {
0251         result.setError(i18n("GPU acceleration is turned off in Kdenlive settings, but is required for this project's Movit filters."));
0252         return result;
0253     }
0254 
0255     DocumentChecker d(url, domDoc);
0256 
0257     if (d.hasErrorInProject()) {
0258         if (pCore->window() == nullptr) {
0259             qInfo() << "DocumentChecker found some problems in the project:";
0260             for (const auto &item : d.resourceItems()) {
0261                 qInfo() << item;
0262                 if (item.status == DocumentChecker::MissingStatus::Missing) {
0263                     success = false;
0264                 }
0265             }
0266         } else {
0267             success = d.resolveProblemsWithGUI();
0268         }
0269     }
0270 
0271     if (!success) {
0272         // Loading aborted
0273         result.setAborted();
0274         return result;
0275     }
0276 
0277     // create KdenliveDoc object
0278     auto doc = std::unique_ptr<KdenliveDoc>(new KdenliveDoc(url, domDoc, projectFolder, undoGroup, parent));
0279     if (!validationResult.second.isEmpty()) {
0280         doc->m_modifiedDecimalPoint = validationResult.second;
0281         //doc->setModifiedDecimalPoint(validationResult.second);
0282     }
0283     doc->loadDocumentProperties();
0284     if (!doc->m_projectFolder.isEmpty()) {
0285         // Ask to create the project directory if it does not exist
0286         QDir folder(doc->m_projectFolder);
0287         if (!folder.mkpath(QStringLiteral("."))) {
0288             // Project folder is not writable
0289             doc->m_projectFolder = doc->m_url.toString(QUrl::RemoveFilename | QUrl::RemoveScheme);
0290             folder.setPath(doc->m_projectFolder);
0291             if (folder.exists()) {
0292                 KMessageBox::error(
0293                     parent,
0294                     i18n("The project directory %1, could not be created.\nPlease make sure you have the required permissions.\nDefaulting to system folders",
0295                          doc->m_projectFolder));
0296             } else {
0297                 KMessageBox::information(parent, i18n("Document project folder is invalid, using system default folders"));
0298             }
0299             doc->m_projectFolder.clear();
0300         }
0301     }
0302     doc->initCacheDirs();
0303 
0304     if (doc->m_document.documentElement().hasAttribute(QStringLiteral("upgraded"))) {
0305         doc->m_documentOpenStatus = UpgradedProject;
0306         result.setUpgraded(true);
0307     } else if (doc->m_document.documentElement().hasAttribute(QStringLiteral("modified")) || validator.isModified()) {
0308         doc->m_documentOpenStatus = ModifiedProject;
0309         result.setModified(true);
0310         doc->setModified(true);
0311     }
0312 
0313     if (result.wasModified() || result.wasUpgraded()) {
0314         doc->requestBackup();
0315     }
0316     result.setDocument(std::move(doc));
0317 
0318     return result;
0319 }
0320 
0321 KdenliveDoc::~KdenliveDoc()
0322 {
0323     if (m_url.isEmpty()) {
0324         // Document was never saved, delete cache folder
0325         QString documentId = QDir::cleanPath(m_documentProperties.value(QStringLiteral("documentid")));
0326         bool ok = false;
0327         documentId.toLongLong(&ok, 10);
0328         if (ok && !documentId.isEmpty()) {
0329             QDir baseCache = getCacheDir(CacheBase, &ok);
0330             if (baseCache.dirName() == documentId && baseCache.entryList(QDir::Files).isEmpty()) {
0331                 baseCache.removeRecursively();
0332             }
0333         }
0334     }
0335     // qCDebug(KDENLIVE_LOG) << "// DEL CLP MAN";
0336     disconnect(this, &KdenliveDoc::docModified, pCore->window(), &MainWindow::slotUpdateDocumentState);
0337     m_commandStack->clear();
0338     m_timelines.clear();
0339     // qCDebug(KDENLIVE_LOG) << "// DEL CLP MAN done";
0340     if (m_autosave) {
0341         if (!m_autosave->fileName().isEmpty()) {
0342             m_autosave->remove();
0343         }
0344         delete m_autosave;
0345     }
0346 }
0347 
0348 void KdenliveDoc::initializeProperties(bool newDocument, std::pair<int, int> tracks, int audioChannels)
0349 {
0350     // init default document properties
0351     m_documentProperties[QStringLiteral("enableproxy")] = QString::number(int(KdenliveSettings::enableproxy()));
0352     m_documentProperties[QStringLiteral("proxyparams")] = KdenliveSettings::proxyparams();
0353     m_documentProperties[QStringLiteral("proxyextension")] = KdenliveSettings::proxyextension();
0354     m_documentProperties[QStringLiteral("previewparameters")] = KdenliveSettings::previewparams();
0355     m_documentProperties[QStringLiteral("previewextension")] = KdenliveSettings::previewextension();
0356     m_documentProperties[QStringLiteral("externalproxyparams")] = KdenliveSettings::externalProxyProfile();
0357     m_documentProperties[QStringLiteral("enableexternalproxy")] = QString::number(int(KdenliveSettings::externalproxy()));
0358     m_documentProperties[QStringLiteral("generateproxy")] = QString::number(int(KdenliveSettings::generateproxy()));
0359     m_documentProperties[QStringLiteral("proxyminsize")] = QString::number(KdenliveSettings::proxyminsize());
0360     m_documentProperties[QStringLiteral("generateimageproxy")] = QString::number(int(KdenliveSettings::generateimageproxy()));
0361     m_documentProperties[QStringLiteral("proxyimageminsize")] = QString::number(KdenliveSettings::proxyimageminsize());
0362     m_documentProperties[QStringLiteral("proxyimagesize")] = QString::number(KdenliveSettings::proxyimagesize());
0363     m_documentProperties[QStringLiteral("proxyresize")] = QString::number(KdenliveSettings::proxyscale());
0364     m_documentProperties[QStringLiteral("enableTimelineZone")] = QLatin1Char('0');
0365     m_documentProperties[QStringLiteral("seekOffset")] = QString::number(TimelineModel::seekDuration);
0366     m_documentProperties[QStringLiteral("audioChannels")] = QString::number(audioChannels);
0367     m_documentProperties[QStringLiteral("uuid")] = m_uuid.toString();
0368     if (newDocument) {
0369         QMap<QString, QString> sequenceProperties;
0370         // video tracks are after audio tracks, and the UI shows them from highest position to lowest position
0371         sequenceProperties[QStringLiteral("videoTarget")] = QString::number(tracks.second);
0372         sequenceProperties[QStringLiteral("audioTarget")] = QString::number(tracks.second - 1);
0373         // If there is at least one video track, set activeTrack to be the first
0374         // video track (which comes after the audio tracks). Otherwise, set the
0375         // activeTrack to be the last audio track (the top-most audio track in the
0376         // UI).
0377         const int activeTrack = tracks.first > 0 ? tracks.second : tracks.second - 1;
0378         sequenceProperties[QStringLiteral("activeTrack")] = QString::number(activeTrack);
0379         sequenceProperties[QStringLiteral("documentuuid")] = m_uuid.toString();
0380         m_sequenceProperties.insert(m_uuid, sequenceProperties);
0381         // For existing documents, don't define guidesCategories, so that we can use the getDefaultGuideCategories() for backwards compatibility
0382         m_documentProperties[QStringLiteral("guidesCategories")] = MarkerListModel::categoriesListToJSon(KdenliveSettings::guidesCategories());
0383     }
0384 }
0385 
0386 const QStringList KdenliveDoc::guidesCategories()
0387 {
0388     QStringList categories = getGuideModel(activeUuid)->guideCategoriesToStringList(m_documentProperties.value(QStringLiteral("guidesCategories")));
0389     if (categories.isEmpty()) {
0390         const QStringList defaultCategories = getDefaultGuideCategories();
0391         m_documentProperties[QStringLiteral("guidesCategories")] = MarkerListModel::categoriesListToJSon(defaultCategories);
0392         return defaultCategories;
0393     }
0394     return categories;
0395 }
0396 
0397 void KdenliveDoc::updateGuideCategories(const QStringList &categories, const QMap<int, int> remapCategories)
0398 {
0399     const QStringList currentCategories =
0400         getGuideModel(activeUuid)->guideCategoriesToStringList(m_documentProperties.value(QStringLiteral("guidesCategories")));
0401     // Check if a guide category was removed
0402     QList<int> currentIndexes;
0403     QList<int> updatedIndexes;
0404     for (auto &cat : currentCategories) {
0405         currentIndexes << cat.section(QLatin1Char(':'), -2, -2).toInt();
0406     }
0407     for (auto &cat : categories) {
0408         updatedIndexes << cat.section(QLatin1Char(':'), -2, -2).toInt();
0409     }
0410     for (auto &i : updatedIndexes) {
0411         currentIndexes.removeAll(i);
0412     }
0413     if (!currentIndexes.isEmpty()) {
0414         // A marker category was removed, delete all Bin clip markers using it
0415         pCore->bin()->removeMarkerCategories(currentIndexes, remapCategories);
0416     }
0417     getGuideModel(activeUuid)->loadCategoriesWithUndo(categories, currentCategories, remapCategories);
0418 }
0419 
0420 void KdenliveDoc::saveGuideCategories()
0421 {
0422     const QString categories = getGuideModel(activeUuid)->categoriesToJSon();
0423     m_documentProperties[QStringLiteral("guidesCategories")] = categories;
0424 }
0425 
0426 int KdenliveDoc::updateClipsCount()
0427 {
0428     m_clipsCount = m_document.elementsByTagName(QLatin1String("entry")).size();
0429     return m_clipsCount;
0430 }
0431 
0432 int KdenliveDoc::clipsCount() const
0433 {
0434     return m_clipsCount;
0435 }
0436 
0437 const QByteArray KdenliveDoc::getAndClearProjectXml()
0438 {
0439     // Profile has already been set, dont overwrite it
0440     m_document.documentElement().removeChild(m_document.documentElement().firstChildElement(QLatin1String("profile")));
0441     const QByteArray result = m_document.toString().toUtf8();
0442     // We don't need the xml data anymore, throw away
0443     m_document.clear();
0444     return result;
0445 }
0446 
0447 QDomDocument KdenliveDoc::createEmptyDocument(int videotracks, int audiotracks, bool disableProfile)
0448 {
0449     QList<TrackInfo> tracks;
0450     // Tracks are added «backwards», so we need to reverse the track numbering
0451     // mbt 331: http://www.kdenlive.org/mantis/view.php?id=331
0452     // Better default names for tracks: Audio 1 etc. instead of blank numbers
0453     tracks.reserve(audiotracks + videotracks);
0454     for (int i = 0; i < audiotracks; ++i) {
0455         TrackInfo audioTrack;
0456         audioTrack.type = AudioTrack;
0457         audioTrack.isMute = false;
0458         audioTrack.isBlind = true;
0459         audioTrack.isLocked = false;
0460         // audioTrack.trackName = i18n("Audio %1", audiotracks - i);
0461         audioTrack.duration = 0;
0462         tracks.append(audioTrack);
0463     }
0464     for (int i = 0; i < videotracks; ++i) {
0465         TrackInfo videoTrack;
0466         videoTrack.type = VideoTrack;
0467         videoTrack.isMute = false;
0468         videoTrack.isBlind = false;
0469         videoTrack.isLocked = false;
0470         // videoTrack.trackName = i18n("Video %1", i + 1);
0471         videoTrack.duration = 0;
0472         tracks.append(videoTrack);
0473     }
0474     return createEmptyDocument(tracks, disableProfile);
0475 }
0476 
0477 QDomDocument KdenliveDoc::createEmptyDocument(const QList<TrackInfo> &tracks, bool disableProfile)
0478 {
0479     // Creating new document
0480     QDomDocument doc;
0481     std::unique_ptr<Mlt::Profile> docProfile(new Mlt::Profile(pCore->getCurrentProfilePath().toUtf8().constData()));
0482     Mlt::Consumer xmlConsumer(*docProfile.get(), "xml:kdenlive_playlist");
0483     if (disableProfile) {
0484         xmlConsumer.set("no_profile", 1);
0485     }
0486     xmlConsumer.set("terminate_on_pause", 1);
0487     xmlConsumer.set("store", "kdenlive");
0488     Mlt::Tractor tractor(*docProfile.get());
0489     Mlt::Producer bk(*docProfile.get(), "color:black");
0490     bk.set("mlt_image_format", "rgba");
0491     tractor.insert_track(bk, 0);
0492     for (int i = 0; i < tracks.count(); ++i) {
0493         Mlt::Tractor track(*docProfile.get());
0494         track.set("kdenlive:track_name", tracks.at(i).trackName.toUtf8().constData());
0495         track.set("kdenlive:timeline_active", 1);
0496         track.set("kdenlive:trackheight", KdenliveSettings::trackheight());
0497         if (tracks.at(i).type == AudioTrack) {
0498             track.set("kdenlive:audio_track", 1);
0499         }
0500         if (tracks.at(i).isLocked) {
0501             track.set("kdenlive:locked_track", 1);
0502         }
0503         if (tracks.at(i).isMute) {
0504             if (tracks.at(i).isBlind) {
0505                 track.set("hide", 3);
0506             } else {
0507                 track.set("hide", 2);
0508             }
0509         } else if (tracks.at(i).isBlind) {
0510             track.set("hide", 1);
0511         }
0512         Mlt::Playlist playlist1(*docProfile.get());
0513         Mlt::Playlist playlist2(*docProfile.get());
0514         track.insert_track(playlist1, 0);
0515         track.insert_track(playlist2, 1);
0516         tractor.insert_track(track, i + 1);
0517     }
0518     QScopedPointer<Mlt::Field> field(tractor.field());
0519     QString compositeService = TransitionsRepository::get()->getCompositingTransition();
0520     if (!compositeService.isEmpty()) {
0521         for (int i = 0; i <= tracks.count(); i++) {
0522             if (i > 0 && tracks.at(i - 1).type == AudioTrack) {
0523                 Mlt::Transition tr(*docProfile.get(), "mix");
0524                 tr.set("a_track", 0);
0525                 tr.set("b_track", i);
0526                 tr.set("always_active", 1);
0527                 tr.set("sum", 1);
0528                 tr.set("accepts_blanks", 1);
0529                 tr.set("internal_added", 237);
0530                 field->plant_transition(tr, 0, i);
0531             }
0532             if (i > 0 && tracks.at(i - 1).type == VideoTrack) {
0533                 Mlt::Transition tr(*docProfile.get(), compositeService.toUtf8().constData());
0534                 tr.set("a_track", 0);
0535                 tr.set("b_track", i);
0536                 tr.set("always_active", 1);
0537                 tr.set("internal_added", 237);
0538                 field->plant_transition(tr, 0, i);
0539             }
0540         }
0541     }
0542     Mlt::Producer prod(tractor.get_producer());
0543     xmlConsumer.connect(prod);
0544     xmlConsumer.run();
0545     QString playlist = QString::fromUtf8(xmlConsumer.get("kdenlive_playlist"));
0546     doc.setContent(playlist);
0547     return doc;
0548 }
0549 
0550 bool KdenliveDoc::useProxy() const
0551 {
0552     return m_documentProperties.value(QStringLiteral("enableproxy")).toInt() != 0;
0553 }
0554 
0555 bool KdenliveDoc::useExternalProxy() const
0556 {
0557     return m_documentProperties.value(QStringLiteral("enableexternalproxy")).toInt() != 0;
0558 }
0559 
0560 bool KdenliveDoc::autoGenerateProxy(int width) const
0561 {
0562     return (m_documentProperties.value(QStringLiteral("generateproxy")).toInt() != 0) &&
0563            (width < 0 || width > m_documentProperties.value(QStringLiteral("proxyminsize")).toInt());
0564 }
0565 
0566 bool KdenliveDoc::autoGenerateImageProxy(int width) const
0567 {
0568     return (m_documentProperties.value(QStringLiteral("generateimageproxy")).toInt() != 0) &&
0569            (width < 0 || width > m_documentProperties.value(QStringLiteral("proxyimageminsize")).toInt());
0570 }
0571 
0572 void KdenliveDoc::slotAutoSave(const QString &scene)
0573 {
0574     if (m_autosave != nullptr) {
0575         if (!m_autosave->isOpen() && !m_autosave->open(QIODevice::ReadWrite)) {
0576             // show error: could not open the autosave file
0577             qCDebug(KDENLIVE_LOG) << "ERROR; CANNOT CREATE AUTOSAVE FILE";
0578             pCore->displayMessage(i18n("Cannot create autosave file %1", m_autosave->fileName()), ErrorMessage);
0579             return;
0580         }
0581         if (scene.isEmpty()) {
0582             // Make sure we don't save if scenelist is corrupted
0583             KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", m_autosave->fileName()));
0584             return;
0585         }
0586         m_autosave->resize(0);
0587         if (m_autosave->write(scene.toUtf8()) < 0) {
0588             pCore->displayMessage(i18n("Cannot create autosave file %1", m_autosave->fileName()), ErrorMessage);
0589         }
0590         m_autosave->flush();
0591     }
0592 }
0593 
0594 void KdenliveDoc::setZoom(const QUuid &uuid, int horizontal, int vertical)
0595 {
0596     setSequenceProperty(uuid, QStringLiteral("zoom"), horizontal);
0597     if (vertical > -1) {
0598         setSequenceProperty(uuid, QStringLiteral("verticalzoom"), vertical);
0599     }
0600 }
0601 
0602 void KdenliveDoc::importSequenceProperties(const QUuid uuid, const QStringList properties)
0603 {
0604     for (const auto &prop : properties) {
0605         if (m_documentProperties.contains(prop)) {
0606             setSequenceProperty(uuid, prop, m_documentProperties.value(prop));
0607         }
0608     }
0609     for (const auto &prop : properties) {
0610         m_documentProperties.remove(prop);
0611     }
0612 }
0613 
0614 QPoint KdenliveDoc::zoom(const QUuid &uuid) const
0615 {
0616     return QPoint(getSequenceProperty(uuid, QStringLiteral("zoom"), QStringLiteral("8")).toInt(),
0617                   getSequenceProperty(uuid, QStringLiteral("verticalzoom")).toInt());
0618 }
0619 
0620 void KdenliveDoc::setZone(const QUuid &uuid, int start, int end)
0621 {
0622     setSequenceProperty(uuid, QStringLiteral("zonein"), start);
0623     setSequenceProperty(uuid, QStringLiteral("zoneout"), end);
0624 }
0625 
0626 QPoint KdenliveDoc::zone(const QUuid &uuid) const
0627 {
0628     return QPoint(getSequenceProperty(uuid, QStringLiteral("zonein")).toInt(), getSequenceProperty(uuid, QStringLiteral("zoneout")).toInt());
0629 }
0630 
0631 QPair<int, int> KdenliveDoc::targetTracks(const QUuid &uuid) const
0632 {
0633     return {getSequenceProperty(uuid, QStringLiteral("videoTarget")).toInt(), getSequenceProperty(uuid, QStringLiteral("audioTarget")).toInt()};
0634 }
0635 
0636 QDomDocument KdenliveDoc::xmlSceneList(const QString &scene)
0637 {
0638     QDomDocument sceneList;
0639     sceneList.setContent(scene, true);
0640     QDomElement mlt = sceneList.firstChildElement(QStringLiteral("mlt"));
0641     if (mlt.isNull() || !mlt.hasChildNodes()) {
0642         // scenelist is corrupted
0643         return QDomDocument();
0644     }
0645 
0646     // Set playlist audio volume to 100%
0647     QDomNodeList tractors = mlt.elementsByTagName(QStringLiteral("tractor"));
0648     for (int i = 0; i < tractors.count(); ++i) {
0649         if (tractors.at(i).toElement().hasAttribute(QStringLiteral("global_feed"))) {
0650             // This is our main tractor
0651             QDomElement tractor = tractors.at(i).toElement();
0652             if (Xml::hasXmlProperty(tractor, QLatin1String("meta.volume"))) {
0653                 Xml::setXmlProperty(tractor, QStringLiteral("meta.volume"), QStringLiteral("1"));
0654             }
0655             break;
0656         }
0657     }
0658     QDomNodeList tracks = mlt.elementsByTagName(QStringLiteral("track"));
0659     if (tracks.isEmpty()) {
0660         // Something is very wrong, inform user.
0661         qDebug() << " = = = =  = =  CORRUPTED DOC\n" << scene;
0662         return QDomDocument();
0663     }
0664 
0665     QDomNodeList pls = mlt.elementsByTagName(QStringLiteral("playlist"));
0666     QDomElement mainPlaylist;
0667     for (int i = 0; i < pls.count(); ++i) {
0668         if (pls.at(i).toElement().attribute(QStringLiteral("id")) == BinPlaylist::binPlaylistId) {
0669             mainPlaylist = pls.at(i).toElement();
0670             break;
0671         }
0672     }
0673 
0674     // check if project contains custom effects to embed them in project file
0675     QDomNodeList effects = mlt.elementsByTagName(QStringLiteral("filter"));
0676     int maxEffects = effects.count();
0677     // qCDebug(KDENLIVE_LOG) << "// FOUD " << maxEffects << " EFFECTS+++++++++++++++++++++";
0678     QMap<QString, QString> effectIds;
0679     for (int i = 0; i < maxEffects; ++i) {
0680         QDomNode m = effects.at(i);
0681         QDomNodeList params = m.childNodes();
0682         QString id;
0683         QString tag;
0684         for (int j = 0; j < params.count(); ++j) {
0685             QDomElement e = params.item(j).toElement();
0686             if (e.attribute(QStringLiteral("name")) == QLatin1String("kdenlive_id")) {
0687                 id = e.firstChild().nodeValue();
0688             }
0689             if (e.attribute(QStringLiteral("name")) == QLatin1String("tag")) {
0690                 tag = e.firstChild().nodeValue();
0691             }
0692             if (!id.isEmpty() && !tag.isEmpty()) {
0693                 effectIds.insert(id, tag);
0694             }
0695         }
0696     }
0697     // TODO: find a way to process this before rendering MLT scenelist to xml
0698     /*QDomDocument customeffects = initEffects::getUsedCustomEffects(effectIds);
0699     if (!customeffects.documentElement().childNodes().isEmpty()) {
0700         Xml::setXmlProperty(mainPlaylist, QStringLiteral("kdenlive:customeffects"), customeffects.toString());
0701     }*/
0702     // addedXml.appendChild(sceneList.importNode(customeffects.documentElement(), true));
0703 
0704     // TODO: move metadata to previous step in saving process
0705     QDomElement docmetadata = sceneList.createElement(QStringLiteral("documentmetadata"));
0706     QMapIterator<QString, QString> j(m_documentMetadata);
0707     while (j.hasNext()) {
0708         j.next();
0709         docmetadata.setAttribute(j.key(), j.value());
0710     }
0711     // addedXml.appendChild(docmetadata);
0712 
0713     return sceneList;
0714 }
0715 
0716 bool KdenliveDoc::saveSceneList(const QString &path, const QString &scene, bool saveOverExistingFile)
0717 {
0718     QDomDocument sceneList = xmlSceneList(scene);
0719     if (sceneList.isNull()) {
0720         // Make sure we don't save if scenelist is corrupted
0721         KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", path));
0722         return false;
0723     }
0724 
0725     // Backup current version
0726     backupLastSavedVersion(path);
0727     if (m_documentOpenStatus != CleanProject && saveOverExistingFile) {
0728         // create visible backup file and warn user
0729         QString baseFile = path.section(QStringLiteral(".kdenlive"), 0, 0);
0730         int ct = 0;
0731         QString backupFile = baseFile + QStringLiteral("_backup") + QString::number(ct) + QStringLiteral(".kdenlive");
0732         while (QFile::exists(backupFile)) {
0733             ct++;
0734             backupFile = baseFile + QStringLiteral("_backup") + QString::number(ct) + QStringLiteral(".kdenlive");
0735         }
0736         QString message;
0737         if (m_documentOpenStatus == UpgradedProject) {
0738             message =
0739                 i18n("Your project file was upgraded to the latest Kdenlive document version.\nTo make sure you do not lose data, a backup copy called %1 "
0740                      "was created.",
0741                      backupFile);
0742         } else {
0743             message = i18n("Your project file was modified by Kdenlive.\nTo make sure you do not lose data, a backup copy called %1 was created.", backupFile);
0744         }
0745 
0746         KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile));
0747         if (copyjob->exec()) {
0748             KMessageBox::information(QApplication::activeWindow(), message);
0749             m_documentOpenStatus = CleanProject;
0750         } else {
0751             KMessageBox::information(
0752                 QApplication::activeWindow(),
0753                 i18n("Your project file was upgraded to the latest Kdenlive document version, but it was not possible to create the backup copy %1.",
0754                      backupFile));
0755         }
0756     }
0757     QSaveFile file(path);
0758     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
0759         qCWarning(KDENLIVE_LOG) << "//////  ERROR writing to file: " << path;
0760         KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path));
0761         return false;
0762     }
0763 
0764     const QByteArray sceneData = sceneList.toString().toUtf8();
0765 
0766     file.write(sceneData);
0767     if (!file.commit()) {
0768         KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path));
0769         return false;
0770     }
0771     cleanupBackupFiles();
0772     QFileInfo info(path);
0773     QString fileName = QUrl::fromLocalFile(path).fileName().section(QLatin1Char('.'), 0, -2);
0774     fileName.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid")));
0775     fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm")));
0776     fileName.append(QStringLiteral(".kdenlive.png"));
0777     QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup"));
0778     Q_EMIT saveTimelinePreview(backupFolder.absoluteFilePath(fileName));
0779     return true;
0780 }
0781 
0782 QString KdenliveDoc::projectTempFolder() const
0783 {
0784     if (m_projectFolder.isEmpty()) {
0785         return QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
0786     }
0787     return m_projectFolder;
0788 }
0789 
0790 QString KdenliveDoc::projectDataFolder(const QString &newPath, bool folderForAudio) const
0791 {
0792     if (folderForAudio) {
0793         if (KdenliveSettings::capturetoprojectfolder() == 2 && !KdenliveSettings::capturefolder().isEmpty()) {
0794             return KdenliveSettings::capturefolder();
0795         }
0796         if (m_projectFolder.isEmpty()) {
0797             // Project has not been saved yet
0798             if (KdenliveSettings::customprojectfolder()) {
0799                 return KdenliveSettings::defaultprojectfolder();
0800             }
0801             return QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
0802         }
0803         if (KdenliveSettings::capturetoprojectfolder() == 1 || m_sameProjectFolder) {
0804             // Always render to project folder
0805             if (KdenliveSettings::customprojectfolder() && !m_sameProjectFolder) {
0806                 return KdenliveSettings::defaultprojectfolder();
0807             }
0808             return QFileInfo(m_url.toLocalFile()).absolutePath();
0809         }
0810         return QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
0811     }
0812     if (KdenliveSettings::videotodefaultfolder() == 2 && !KdenliveSettings::videofolder().isEmpty()) {
0813         return KdenliveSettings::videofolder();
0814     }
0815     if (!newPath.isEmpty() && (KdenliveSettings::videotodefaultfolder() == 1 || m_sameProjectFolder)) {
0816         // Always render to project folder
0817         return newPath;
0818     }
0819     if (m_projectFolder.isEmpty()) {
0820         // Project has not been saved yet
0821         if (KdenliveSettings::customprojectfolder()) {
0822             return KdenliveSettings::defaultprojectfolder();
0823         }
0824         return QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
0825     }
0826     if (KdenliveSettings::videotodefaultfolder() == 1 || m_sameProjectFolder) {
0827         // Always render to project folder
0828         if (KdenliveSettings::customprojectfolder() && !m_sameProjectFolder) {
0829             return KdenliveSettings::defaultprojectfolder();
0830         }
0831         return QFileInfo(m_url.toLocalFile()).absolutePath();
0832     }
0833     return QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
0834 }
0835 
0836 void KdenliveDoc::setProjectFolder(const QUrl &url)
0837 {
0838     if (url == QUrl::fromLocalFile(m_projectFolder)) {
0839         return;
0840     }
0841     setModified(true);
0842     QDir dir(url.toLocalFile());
0843     if (!dir.exists()) {
0844         dir.mkpath(dir.absolutePath());
0845     }
0846     dir.mkdir(QStringLiteral("titles"));
0847     /*if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("You have changed the project folder. Do you want to copy the cached data from %1 to the
0848      * new folder %2?", m_projectFolder, url.path())) == KMessageBox::Yes) moveProjectData(url);*/
0849     m_projectFolder = url.toLocalFile();
0850 
0851     updateProjectFolderPlacesEntry();
0852 }
0853 
0854 const QList<QUrl> KdenliveDoc::getProjectData(bool *ok)
0855 {
0856     // Move proxies
0857     QList<QUrl> cacheUrls;
0858     auto binClips = pCore->projectItemModel()->getAllClipIds();
0859     QDir proxyFolder = getCacheDir(CacheProxy, ok);
0860     if (!*ok) {
0861         qWarning() << "Cannot write to cache folder: " << proxyFolder.absolutePath();
0862         return cacheUrls;
0863     }
0864     // First step: all clips referenced by the bin model exist and are inserted
0865     for (const auto &binClip : binClips) {
0866         auto projClip = pCore->projectItemModel()->getClipByBinID(binClip);
0867         QString proxy = projClip->getProducerProperty(QStringLiteral("kdenlive:proxy"));
0868         QFileInfo p(proxy);
0869         if (proxy.length() > 2 && p.exists() && p.absoluteDir() == proxyFolder) {
0870             // Only move proxy clips that are inside our own proxy folder, not external ones.
0871             QUrl pUrl = QUrl::fromLocalFile(proxy);
0872             if (!cacheUrls.contains(pUrl)) {
0873                 cacheUrls << pUrl;
0874             }
0875         }
0876     }
0877     *ok = true;
0878     return cacheUrls;
0879 }
0880 
0881 void KdenliveDoc::slotMoveFinished(KJob *job)
0882 {
0883     if (job->error() != 0) {
0884         KMessageBox::error(pCore->window(), i18n("Error moving project folder: %1", job->errorText()));
0885     }
0886 }
0887 
0888 bool KdenliveDoc::profileChanged(const QString &profile) const
0889 {
0890     return !(*pCore->getCurrentProfile().get() == *ProfileRepository::get()->getProfile(profile).get());
0891 }
0892 
0893 Render *KdenliveDoc::renderer()
0894 {
0895     return nullptr;
0896 }
0897 
0898 std::shared_ptr<DocUndoStack> KdenliveDoc::commandStack()
0899 {
0900     return m_commandStack;
0901 }
0902 
0903 int KdenliveDoc::getFramePos(const QString &duration)
0904 {
0905     return m_timecode.getFrameCount(duration);
0906 }
0907 
0908 Timecode KdenliveDoc::timecode() const
0909 {
0910     return m_timecode;
0911 }
0912 
0913 int KdenliveDoc::width() const
0914 {
0915     return pCore->getCurrentProfile()->width();
0916 }
0917 
0918 int KdenliveDoc::height() const
0919 {
0920     return pCore->getCurrentProfile()->height();
0921 }
0922 
0923 QUrl KdenliveDoc::url() const
0924 {
0925     return m_url;
0926 }
0927 
0928 void KdenliveDoc::setUrl(const QUrl &url)
0929 {
0930     m_url = url;
0931 }
0932 
0933 QStringList KdenliveDoc::getAllSubtitlesPath(bool final)
0934 {
0935     QStringList result;
0936     QMapIterator<QUuid, std::shared_ptr<TimelineItemModel>> j(m_timelines);
0937     while (j.hasNext()) {
0938         j.next();
0939         if (j.value()->hasSubtitleModel()) {
0940             QMap<std::pair<int, QString>, QString> allSubFiles = j.value()->getSubtitleModel()->getSubtitlesList();
0941             QMapIterator<std::pair<int, QString>, QString> k(allSubFiles);
0942             while (k.hasNext()) {
0943                 k.next();
0944                 result << subTitlePath(j.value()->uuid(), k.key().first, final);
0945             }
0946         }
0947     }
0948     return result;
0949 }
0950 
0951 void KdenliveDoc::prepareRenderAssets(const QDir &destFolder)
0952 {
0953     // Copy current subtitles to assets render folder
0954     updateWorkFilesBeforeSave(destFolder.absoluteFilePath(m_url.fileName()), true);
0955 }
0956 
0957 void KdenliveDoc::restoreRenderAssets()
0958 {
0959     // Copy current subtitles to assets render folder
0960     updateWorkFilesAfterSave();
0961 }
0962 
0963 void KdenliveDoc::updateWorkFilesBeforeSave(const QString &newUrl, bool onRender)
0964 {
0965     QMapIterator<QUuid, std::shared_ptr<TimelineItemModel>> j(m_timelines);
0966     bool checkOverwrite = QUrl::fromLocalFile(newUrl) != m_url;
0967     while (j.hasNext()) {
0968         j.next();
0969         if (j.value()->hasSubtitleModel()) {
0970             // Calculate the new path for each subtitle in this timeline
0971             QString basePath = newUrl;
0972             if (j.value()->uuid() != m_uuid) {
0973                 basePath.append(j.value()->uuid().toString());
0974             }
0975             QMap<std::pair<int, QString>, QString> allSubs = j.value()->getSubtitleModel()->getSubtitlesList();
0976             QMapIterator<std::pair<int, QString>, QString> i(allSubs);
0977             while (i.hasNext()) {
0978                 i.next();
0979                 QString finalName = basePath;
0980                 if (i.key().first > 0) {
0981                     basePath.append(QStringLiteral("-%1").arg(i.key().first));
0982                 }
0983                 QFileInfo info(basePath);
0984                 QString subPath = info.dir().absoluteFilePath(QString("%1.srt").arg(info.fileName()));
0985                 j.value()->getSubtitleModel()->copySubtitle(subPath, i.key().first, checkOverwrite, true);
0986             }
0987         }
0988     }
0989     QDir sequenceFolder;
0990     if (onRender) {
0991         sequenceFolder = QFileInfo(newUrl).dir();
0992         sequenceFolder.mkpath(QFileInfo(newUrl).baseName());
0993         sequenceFolder.cd(QFileInfo(newUrl).baseName());
0994     } else {
0995         bool ok;
0996         sequenceFolder = getCacheDir(CacheSequence, &ok);
0997         if (!ok) {
0998             // Warning, could not access project folder...
0999             qWarning() << "Cannot write to cache folder: " << sequenceFolder.absolutePath();
1000         }
1001     }
1002     pCore->bin()->moveTimeWarpToFolder(sequenceFolder, true);
1003 }
1004 
1005 void KdenliveDoc::updateWorkFilesAfterSave()
1006 {
1007     QMapIterator<QUuid, std::shared_ptr<TimelineItemModel>> j(m_timelines);
1008     while (j.hasNext()) {
1009         j.next();
1010         if (j.value()->hasSubtitleModel()) {
1011             int ix = getSequenceProperty(j.value()->uuid(), QStringLiteral("kdenlive:activeSubtitleIndex"), QStringLiteral("0")).toInt();
1012             j.value()->getSubtitleModel()->restoreTmpFile(ix);
1013         }
1014     }
1015 
1016     bool ok;
1017     QDir sequenceFolder = getCacheDir(CacheTmpWorkFiles, &ok);
1018     pCore->bin()->moveTimeWarpToFolder(sequenceFolder, false);
1019 }
1020 
1021 void KdenliveDoc::slotModified()
1022 {
1023     setModified(!m_commandStack->isClean());
1024 }
1025 
1026 void KdenliveDoc::setModified(bool mod)
1027 {
1028     // fix mantis#3160: The document may have an empty URL if not saved yet, but should have a m_autosave in any case
1029     if ((m_autosave != nullptr) && mod && KdenliveSettings::crashrecovery()) {
1030         Q_EMIT startAutoSave();
1031     }
1032     // TODO: this is not working in case of undo/redo
1033     m_sequenceThumbsNeedsRefresh.insert(pCore->currentTimelineId());
1034 
1035     if (mod == m_modified) {
1036         return;
1037     }
1038     m_modified = mod;
1039     Q_EMIT docModified(m_modified);
1040 }
1041 
1042 bool KdenliveDoc::sequenceThumbRequiresRefresh(const QUuid &uuid) const
1043 {
1044     return m_sequenceThumbsNeedsRefresh.contains(uuid);
1045 }
1046 
1047 void KdenliveDoc::setSequenceThumbRequiresUpdate(const QUuid &uuid)
1048 {
1049     m_sequenceThumbsNeedsRefresh.insert(uuid);
1050 }
1051 
1052 void KdenliveDoc::sequenceThumbUpdated(const QUuid &uuid)
1053 {
1054     m_sequenceThumbsNeedsRefresh.remove(uuid);
1055 }
1056 
1057 bool KdenliveDoc::isModified() const
1058 {
1059     return m_modified;
1060 }
1061 
1062 void KdenliveDoc::requestBackup()
1063 {
1064     m_document.documentElement().setAttribute(QStringLiteral("modified"), 1);
1065 }
1066 
1067 const QString KdenliveDoc::description(const QString suffix) const
1068 {
1069     QString fullName = suffix;
1070     if (!fullName.isEmpty()) {
1071         fullName.append(QLatin1Char(':'));
1072     }
1073     if (!m_url.isValid()) {
1074         fullName.append(i18n("Untitled"));
1075     } else {
1076         fullName.append(QFileInfo(m_url.toLocalFile()).completeBaseName());
1077     }
1078     fullName.append(QStringLiteral(" [*]/ ") + pCore->getCurrentProfile()->description());
1079     return fullName;
1080 }
1081 
1082 QString KdenliveDoc::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
1083 {
1084     QString foundFileName;
1085     QByteArray fileData;
1086     QByteArray fileHash;
1087     QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
1088     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) {
1089         QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
1090         if (file.open(QIODevice::ReadOnly)) {
1091             if (QString::number(file.size()) == matchSize) {
1092                 /*
1093                  * 1 MB = 1 second per 450 files (or faster)
1094                  * 10 MB = 9 seconds per 450 files (or faster)
1095                  */
1096                 if (file.size() > 1000000 * 2) {
1097                     fileData = file.read(1000000);
1098                     if (file.seek(file.size() - 1000000)) {
1099                         fileData.append(file.readAll());
1100                     }
1101                 } else {
1102                     fileData = file.readAll();
1103                 }
1104                 file.close();
1105                 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
1106                 if (QString::fromLatin1(fileHash.toHex()) == matchHash) {
1107                     return file.fileName();
1108                 }
1109                 qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << "size match but not hash";
1110             }
1111         }
1112         ////qCDebug(KDENLIVE_LOG) << filesAndDirs.at(i) << file.size() << fileHash.toHex();
1113     }
1114     filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
1115     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) {
1116         foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
1117         if (!foundFileName.isEmpty()) {
1118             break;
1119         }
1120     }
1121     return foundFileName;
1122 }
1123 
1124 QStringList KdenliveDoc::getBinFolderClipIds(const QString &folderId) const
1125 {
1126     return pCore->bin()->getBinFolderClipIds(folderId);
1127 }
1128 
1129 void KdenliveDoc::slotCreateTextTemplateClip(const QString &group, const QString &groupId, QUrl path)
1130 {
1131     Q_UNUSED(group)
1132     // TODO refac: this seem to be a duplicate of ClipCreationDialog::createTitleTemplateClip. See if we can merge
1133     QString titlesFolder = QDir::cleanPath(m_projectFolder + QStringLiteral("/titles/"));
1134     if (path.isEmpty()) {
1135         QPointer<QFileDialog> d = new QFileDialog(QApplication::activeWindow(), i18nc("@title:window", "Enter Template Path"), titlesFolder);
1136         d->setMimeTypeFilters(QStringList() << QStringLiteral("application/x-kdenlivetitle"));
1137         d->setFileMode(QFileDialog::ExistingFile);
1138         if (d->exec() == QDialog::Accepted && !d->selectedUrls().isEmpty()) {
1139             path = d->selectedUrls().first();
1140         }
1141         delete d;
1142     }
1143 
1144     if (path.isEmpty()) {
1145         return;
1146     }
1147 
1148     // TODO: rewrite with new title system (just set resource)
1149     QString id = ClipCreator::createTitleTemplate(path.toString(), QString(), i18n("Template title clip"), groupId, pCore->projectItemModel());
1150     Q_EMIT selectLastAddedClip(id);
1151 }
1152 
1153 void KdenliveDoc::cacheImage(const QString &fileId, const QImage &img) const
1154 {
1155     bool ok;
1156     QDir dir = getCacheDir(CacheThumbs, &ok);
1157     if (ok) {
1158         img.save(dir.absoluteFilePath(fileId + QStringLiteral(".png")));
1159     }
1160 }
1161 
1162 void KdenliveDoc::setDocumentProperty(const QString &name, const QString &value)
1163 {
1164     if (value.isEmpty()) {
1165         m_documentProperties.remove(name);
1166         return;
1167     }
1168     m_documentProperties[name] = value;
1169 }
1170 
1171 const QString KdenliveDoc::getDocumentProperty(const QString &name, const QString &defaultValue) const
1172 {
1173     return m_documentProperties.value(name, defaultValue);
1174 }
1175 
1176 bool KdenliveDoc::hasDocumentProperty(const QString &name) const
1177 {
1178     return m_documentProperties.contains(name);
1179 }
1180 
1181 void KdenliveDoc::setSequenceProperty(const QUuid &uuid, const QString &name, const QString &value)
1182 {
1183     if (m_sequenceProperties.contains(uuid)) {
1184         if (value.isEmpty()) {
1185             m_sequenceProperties[uuid].remove(name);
1186         } else {
1187             m_sequenceProperties[uuid].insert(name, value);
1188         }
1189     } else if (!value.isEmpty()) {
1190         QMap<QString, QString> sequenceMap;
1191         sequenceMap.insert(name, value);
1192         m_sequenceProperties.insert(uuid, sequenceMap);
1193     }
1194 }
1195 
1196 void KdenliveDoc::setSequenceProperty(const QUuid &uuid, const QString &name, int value)
1197 {
1198     setSequenceProperty(uuid, name, QString::number(value));
1199 }
1200 
1201 const QString KdenliveDoc::getSequenceProperty(const QUuid &uuid, const QString &name, const QString defaultValue) const
1202 {
1203     if (m_sequenceProperties.contains(uuid)) {
1204         const QMap<QString, QString> sequenceMap = m_sequenceProperties.value(uuid);
1205         const QString result = sequenceMap.value(name, defaultValue);
1206         return result;
1207     }
1208     return defaultValue;
1209 }
1210 
1211 bool KdenliveDoc::hasSequenceProperty(const QUuid &uuid, const QString &name) const
1212 {
1213     if (m_sequenceProperties.contains(uuid)) {
1214         if (m_sequenceProperties.value(uuid).contains(name)) {
1215             return true;
1216         }
1217     }
1218     return false;
1219 }
1220 
1221 void KdenliveDoc::clearSequenceProperty(const QUuid &uuid, const QString &name)
1222 {
1223     if (m_sequenceProperties.contains(uuid)) {
1224         m_sequenceProperties[uuid].remove(name);
1225     }
1226 }
1227 
1228 const QMap<QString, QString> KdenliveDoc::getSequenceProperties(const QUuid &uuid) const
1229 {
1230     if (m_sequenceProperties.contains(uuid)) {
1231         return m_sequenceProperties.value(uuid);
1232     }
1233     return QMap<QString, QString>();
1234 }
1235 
1236 QMap<QString, QString> KdenliveDoc::getRenderProperties() const
1237 {
1238     QMap<QString, QString> renderProperties;
1239     QMapIterator<QString, QString> i(m_documentProperties);
1240     while (i.hasNext()) {
1241         i.next();
1242         if (i.key().startsWith(QLatin1String("render"))) {
1243             if (i.key() == QLatin1String("renderurl")) {
1244                 // Check that we have a full path
1245                 QString value = i.value();
1246                 if (QFileInfo(value).isRelative()) {
1247                     value.prepend(m_documentRoot);
1248                 }
1249                 renderProperties.insert(i.key(), value);
1250             } else {
1251                 renderProperties.insert(i.key(), i.value());
1252             }
1253         }
1254     }
1255     return renderProperties;
1256 }
1257 
1258 void KdenliveDoc::saveCustomEffects(const QDomNodeList &customeffects)
1259 {
1260     QDomElement e;
1261     QStringList importedEffects;
1262     int maxchild = customeffects.count();
1263     QStringList newPaths;
1264     for (int i = 0; i < maxchild; ++i) {
1265         e = customeffects.at(i).toElement();
1266         const QString id = e.attribute(QStringLiteral("id"));
1267         if (!id.isEmpty()) {
1268             // Check if effect exists or save it
1269             if (EffectsRepository::get()->exists(id)) {
1270                 QDomDocument doc;
1271                 doc.appendChild(doc.importNode(e, true));
1272                 QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects");
1273                 path += id + QStringLiteral(".xml");
1274                 if (!QFile::exists(path)) {
1275                     importedEffects << id;
1276                     newPaths << path;
1277                     QFile file(path);
1278                     if (file.open(QFile::WriteOnly | QFile::Truncate)) {
1279                         QTextStream out(&file);
1280 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1281                         out.setCodec("UTF-8");
1282 #endif
1283                         out << doc.toString();
1284                     } else {
1285                         KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", file.fileName()));
1286                     }
1287                 }
1288             }
1289         }
1290     }
1291     if (!importedEffects.isEmpty()) {
1292         KMessageBox::informationList(QApplication::activeWindow(), i18n("The following effects were imported from the project:"), importedEffects);
1293     }
1294     if (!importedEffects.isEmpty()) {
1295         Q_EMIT reloadEffects(newPaths);
1296     }
1297 }
1298 
1299 void KdenliveDoc::updateProjectFolderPlacesEntry()
1300 {
1301     /*
1302      * For similar and more code have a look at kfileplacesmodel.cpp and the included files:
1303      * https://api.kde.org/frameworks/kio/html/kfileplacesmodel_8cpp_source.html
1304      */
1305 
1306     const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/user-places.xbel");
1307 #if QT_VERSION_MAJOR < 6
1308     KBookmarkManager *bookmarkManager = KBookmarkManager::managerForExternalFile(file);
1309 #else
1310     std::unique_ptr<KBookmarkManager> bookmarkManager = std::make_unique<KBookmarkManager>(file);
1311 #endif
1312     if (!bookmarkManager) {
1313         return;
1314     }
1315     KBookmarkGroup root = bookmarkManager->root();
1316 
1317     KBookmark bookmark = root.first();
1318 
1319     QString kdenliveName = QCoreApplication::applicationName();
1320     QUrl documentLocation = QUrl::fromLocalFile(m_projectFolder);
1321 
1322     bool exists = false;
1323 
1324     while (!bookmark.isNull()) {
1325         // UDI not empty indicates a device
1326         QString udi = bookmark.metaDataItem(QStringLiteral("UDI"));
1327         QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp"));
1328 
1329         if (udi.isEmpty() && appName == kdenliveName && bookmark.text() == i18n("Project Folder")) {
1330             if (bookmark.url() != documentLocation) {
1331                 bookmark.setUrl(documentLocation);
1332                 bookmarkManager->emitChanged(root);
1333             }
1334             exists = true;
1335             break;
1336         }
1337 
1338         bookmark = root.next(bookmark);
1339     }
1340 
1341     // if entry does not exist yet (was not found), well, create it then
1342     if (!exists) {
1343         bookmark = root.addBookmark(i18n("Project Folder"), documentLocation, QStringLiteral("folder-favorites"));
1344         // Make this user selectable ?
1345         bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), kdenliveName);
1346         bookmarkManager->emitChanged(root);
1347     }
1348 }
1349 
1350 // static
1351 double KdenliveDoc::getDisplayRatio(const QString &path)
1352 {
1353     QDomDocument doc;
1354     if (!Xml::docContentFromFile(doc, path, false)) {
1355         return 0;
1356     }
1357     QDomNodeList list = doc.elementsByTagName(QStringLiteral("profile"));
1358     if (list.isEmpty()) {
1359         return 0;
1360     }
1361     QDomElement profile = list.at(0).toElement();
1362     double den = profile.attribute(QStringLiteral("display_aspect_den")).toDouble();
1363     if (den > 0) {
1364         return profile.attribute(QStringLiteral("display_aspect_num")).toDouble() / den;
1365     }
1366     return 0;
1367 }
1368 
1369 void KdenliveDoc::backupLastSavedVersion(const QString &path)
1370 {
1371     // Ensure backup folder exists
1372     if (path.isEmpty()) {
1373         return;
1374     }
1375     QFile file(path);
1376     QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup"));
1377     QString fileName = QUrl::fromLocalFile(path).fileName().section(QLatin1Char('.'), 0, -2);
1378     QFileInfo info(file);
1379     fileName.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid")));
1380     fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm")));
1381     fileName.append(QStringLiteral(".kdenlive"));
1382     QString backupFile = backupFolder.absoluteFilePath(fileName);
1383     if (file.exists()) {
1384         // delete previous backup if it was done less than 60 seconds ago
1385         QFile::remove(backupFile);
1386         if (!QFile::copy(path, backupFile)) {
1387             KMessageBox::information(QApplication::activeWindow(), i18n("Cannot create backup copy:\n%1", backupFile));
1388         }
1389         // backup subitle file in case we have one
1390         QString subpath(path + QStringLiteral(".srt"));
1391         QString subbackupFile(backupFile + QStringLiteral(".srt"));
1392         if (QFile(subpath).exists()) {
1393             QFile::remove(subbackupFile);
1394             if (!QFile::copy(subpath, subbackupFile)) {
1395                 KMessageBox::information(QApplication::activeWindow(), i18n("Cannot create backup copy:\n%1", subbackupFile));
1396             }
1397         }
1398     }
1399 }
1400 
1401 void KdenliveDoc::cleanupBackupFiles()
1402 {
1403     QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup"));
1404     QString projectFile = url().fileName().section(QLatin1Char('.'), 0, -2);
1405     projectFile.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid")));
1406     projectFile.append(QStringLiteral("-??"));
1407     projectFile.append(QStringLiteral("??"));
1408     projectFile.append(QStringLiteral("-??"));
1409     projectFile.append(QStringLiteral("-??"));
1410     projectFile.append(QStringLiteral("-??"));
1411     projectFile.append(QStringLiteral("-??.kdenlive"));
1412 
1413     QStringList filter;
1414     filter << projectFile;
1415     backupFolder.setNameFilters(filter);
1416     QFileInfoList resultList = backupFolder.entryInfoList(QDir::Files, QDir::Time);
1417 
1418     QDateTime d = QDateTime::currentDateTime();
1419     QStringList hourList;
1420     QStringList dayList;
1421     QStringList weekList;
1422     QStringList oldList;
1423     for (int i = 0; i < resultList.count(); ++i) {
1424         if (d.secsTo(resultList.at(i).lastModified()) < 3600) {
1425             // files created in the last hour
1426             hourList.append(resultList.at(i).absoluteFilePath());
1427         } else if (d.secsTo(resultList.at(i).lastModified()) < 43200) {
1428             // files created in the day
1429             dayList.append(resultList.at(i).absoluteFilePath());
1430         } else if (d.daysTo(resultList.at(i).lastModified()) < 8) {
1431             // files created in the week
1432             weekList.append(resultList.at(i).absoluteFilePath());
1433         } else {
1434             // older files
1435             oldList.append(resultList.at(i).absoluteFilePath());
1436         }
1437     }
1438     if (hourList.count() > 20) {
1439         int step = hourList.count() / 10;
1440         for (int i = 0; i < hourList.count(); i += step) {
1441             // qCDebug(KDENLIVE_LOG)<<"REMOVE AT: "<<i<<", COUNT: "<<hourList.count();
1442             hourList.removeAt(i);
1443             --i;
1444         }
1445     } else {
1446         hourList.clear();
1447     }
1448     if (dayList.count() > 20) {
1449         int step = dayList.count() / 10;
1450         for (int i = 0; i < dayList.count(); i += step) {
1451             dayList.removeAt(i);
1452             --i;
1453         }
1454     } else {
1455         dayList.clear();
1456     }
1457     if (weekList.count() > 20) {
1458         int step = weekList.count() / 10;
1459         for (int i = 0; i < weekList.count(); i += step) {
1460             weekList.removeAt(i);
1461             --i;
1462         }
1463     } else {
1464         weekList.clear();
1465     }
1466     if (oldList.count() > 20) {
1467         int step = oldList.count() / 10;
1468         for (int i = 0; i < oldList.count(); i += step) {
1469             oldList.removeAt(i);
1470             --i;
1471         }
1472     } else {
1473         oldList.clear();
1474     }
1475 
1476     QString f;
1477     while (hourList.count() > 0) {
1478         f = hourList.takeFirst();
1479         QFile::remove(f);
1480         QFile::remove(f + QStringLiteral(".png"));
1481         QFile::remove(f + QStringLiteral(".srt"));
1482     }
1483     while (dayList.count() > 0) {
1484         f = dayList.takeFirst();
1485         QFile::remove(f);
1486         QFile::remove(f + QStringLiteral(".png"));
1487         QFile::remove(f + QStringLiteral(".srt"));
1488     }
1489     while (weekList.count() > 0) {
1490         f = weekList.takeFirst();
1491         QFile::remove(f);
1492         QFile::remove(f + QStringLiteral(".png"));
1493         QFile::remove(f + QStringLiteral(".srt"));
1494     }
1495     while (oldList.count() > 0) {
1496         f = oldList.takeFirst();
1497         QFile::remove(f);
1498         QFile::remove(f + QStringLiteral(".png"));
1499         QFile::remove(f + QStringLiteral(".srt"));
1500     }
1501 }
1502 
1503 const QMap<QString, QString> KdenliveDoc::metadata() const
1504 {
1505     return m_documentMetadata;
1506 }
1507 
1508 void KdenliveDoc::setMetadata(const QMap<QString, QString> &meta)
1509 {
1510     setModified(true);
1511     m_documentMetadata = meta;
1512 }
1513 
1514 QMap<QString, QString> KdenliveDoc::proxyClipsById(const QStringList &ids, bool proxy, const QMap<QString, QString> &proxyPath)
1515 {
1516     QMap<QString, QString> existingProxies;
1517     for (auto &id : ids) {
1518         auto clip = pCore->projectItemModel()->getClipByBinID(id);
1519         QMap<QString, QString> newProps;
1520         if (!proxy) {
1521             newProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-"));
1522             existingProxies.insert(id, clip->getProducerProperty(QStringLiteral("kdenlive:proxy")));
1523         } else if (proxyPath.contains(id)) {
1524             newProps.insert(QStringLiteral("kdenlive:proxy"), proxyPath.value(id));
1525         }
1526         clip->setProperties(newProps);
1527     }
1528     return existingProxies;
1529 }
1530 
1531 void KdenliveDoc::slotProxyCurrentItem(bool doProxy, QList<std::shared_ptr<ProjectClip>> clipList, bool force, QUndoCommand *masterCommand)
1532 {
1533     if (clipList.isEmpty()) {
1534         clipList = pCore->bin()->selectedClips();
1535     }
1536     bool hasParent = true;
1537     if (masterCommand == nullptr) {
1538         masterCommand = new QUndoCommand();
1539         if (doProxy) {
1540             masterCommand->setText(i18np("Add proxy clip", "Add proxy clips", clipList.count()));
1541         } else {
1542             masterCommand->setText(i18np("Remove proxy clip", "Remove proxy clips", clipList.count()));
1543         }
1544         hasParent = false;
1545     }
1546 
1547     // Make sure the proxy folder exists
1548     bool ok;
1549     QDir dir = getCacheDir(CacheProxy, &ok);
1550     if (!ok) {
1551         // Error
1552         qDebug() << "::::: CANNOT GET CACHE DIR!!!!";
1553         return;
1554     }
1555     QString extension = getDocumentProperty(QStringLiteral("proxyextension"));
1556     if (extension.isEmpty()) {
1557         if (m_proxyExtension.isEmpty()) {
1558             initProxySettings();
1559         }
1560         extension = m_proxyExtension;
1561     }
1562     extension.prepend(QLatin1Char('.'));
1563 
1564     // Prepare updated properties
1565     QMap<QString, QString> newProps;
1566     QMap<QString, QString> oldProps;
1567     if (!doProxy) {
1568         newProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-"));
1569     }
1570 
1571     // Parse clips
1572     for (int i = 0; i < clipList.count(); ++i) {
1573         const std::shared_ptr<ProjectClip> &item = clipList.at(i);
1574         ClipType::ProducerType t = item->clipType();
1575         // Only allow proxy on some clip types
1576         if ((t == ClipType::Video || t == ClipType::AV || t == ClipType::Unknown || t == ClipType::Image || t == ClipType::Playlist ||
1577              t == ClipType::SlideShow) &&
1578             item->statusReady()) {
1579             // Check for MP3 with cover art
1580             if (t == ClipType::AV && item->codec(false) == QLatin1String("mjpeg")) {
1581                 QString frame_rate = item->videoCodecProperty(QStringLiteral("frame_rate"));
1582                 if (frame_rate.isEmpty()) {
1583                     frame_rate = item->getProducerProperty(QLatin1String("meta.media.frame_rate_num"));
1584                 }
1585                 if (frame_rate == QLatin1String("90000")) {
1586                     pCore->bin()->doDisplayMessage(i18n("Clip type does not support proxies"), KMessageWidget::Information);
1587                     continue;
1588                 }
1589             }
1590             if ((doProxy && !force && item->hasProxy()) ||
1591                 (!doProxy && !item->hasProxy() && pCore->projectItemModel()->hasClip(item->AbstractProjectItem::clipId()))) {
1592                 continue;
1593             }
1594 
1595             if (doProxy) {
1596                 newProps.clear();
1597                 QString path;
1598                 if (useExternalProxy() && item->hasLimitedDuration()) {
1599                     if (item->hasProducerProperty(QStringLiteral("kdenlive:camcorderproxy"))) {
1600                         const QString camProxy = item->getProducerProperty(QStringLiteral("kdenlive:camcorderproxy"));
1601                         extension = QFileInfo(camProxy).suffix();
1602                         extension.prepend(QLatin1Char('.'));
1603                     } else {
1604                         path = item->getProxyFromOriginal(item->url());
1605                         if (!path.isEmpty()) {
1606                             // Check if source and proxy have the same audio streams count
1607                             int sourceAudioStreams = item->audioStreamsCount();
1608                             std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(pCore->getProjectProfile(), "avformat", path.toUtf8().constData()));
1609                             prod->set("video_index", -1);
1610                             prod->probe();
1611                             auto info = std::make_unique<AudioInfo>(prod);
1612                             int proxyAudioStreams = info->size();
1613                             prod.reset();
1614                             if (proxyAudioStreams != sourceAudioStreams) {
1615                                 // Build a proxy with correct audio streams
1616                                 newProps.insert(QStringLiteral("kdenlive:camcorderproxy"), path);
1617                                 extension = QFileInfo(path).suffix();
1618                                 extension.prepend(QLatin1Char('.'));
1619                                 path.clear();
1620                             }
1621                         }
1622                     }
1623                 }
1624                 if (path.isEmpty()) {
1625                     path = dir.absoluteFilePath(item->hash() + (t == ClipType::Image ? QStringLiteral(".png") : extension));
1626                 }
1627                 newProps.insert(QStringLiteral("kdenlive:proxy"), path);
1628                 // We need to insert empty proxy so that undo will work
1629                 // TODO: how to handle clip properties
1630                 // oldProps = clip->currentProperties(newProps);
1631                 oldProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-"));
1632             } else {
1633                 if (t == ClipType::SlideShow) {
1634                     // Revert to picture aspect ratio
1635                     newProps.insert(QStringLiteral("aspect_ratio"), QStringLiteral("1"));
1636                 }
1637                 // Reset to original url
1638                 newProps.insert(QStringLiteral("resource"), item->url());
1639             }
1640             new EditClipCommand(pCore->bin(), item->AbstractProjectItem::clipId(), oldProps, newProps, true, masterCommand);
1641         } else {
1642             // Cannot proxy this clip type
1643             if (doProxy) {
1644                 pCore->bin()->doDisplayMessage(i18n("Clip type does not support proxies"), KMessageWidget::Information);
1645             }
1646         }
1647     }
1648     if (!hasParent) {
1649         if (masterCommand->childCount() > 0) {
1650             m_commandStack->push(masterCommand);
1651         } else {
1652             delete masterCommand;
1653         }
1654     }
1655 }
1656 
1657 double KdenliveDoc::getDocumentVersion() const
1658 {
1659     return DOCUMENTVERSION;
1660 }
1661 
1662 QMap<QString, QString> KdenliveDoc::documentProperties(bool saveHash)
1663 {
1664     m_documentProperties.insert(QStringLiteral("version"), QString::number(DOCUMENTVERSION));
1665     m_documentProperties.insert(QStringLiteral("kdenliveversion"), QStringLiteral(KDENLIVE_VERSION));
1666     if (!m_projectFolder.isEmpty()) {
1667         QDir folder(m_projectFolder);
1668         m_documentProperties.insert(QStringLiteral("storagefolder"), folder.absoluteFilePath(m_documentProperties.value(QStringLiteral("documentid"))));
1669     }
1670     m_documentProperties.insert(QStringLiteral("profile"), pCore->getCurrentProfile()->path());
1671     if (m_documentProperties.contains(QStringLiteral("decimalPoint"))) {
1672         // "kdenlive:docproperties.decimalPoint" was removed in document version 100
1673         m_documentProperties.remove(QStringLiteral("decimalPoint"));
1674     }
1675     if (pCore->mediaBrowser()) {
1676         m_documentProperties.insert(QStringLiteral("browserurl"), pCore->mediaBrowser()->url().toLocalFile());
1677     }
1678     m_documentProperties.insert(QStringLiteral("binsort"), QString::number(KdenliveSettings::binSorting()));
1679     QMapIterator<QUuid, std::shared_ptr<TimelineItemModel>> j(m_timelines);
1680     while (j.hasNext()) {
1681         j.next();
1682         j.value()->passSequenceProperties(getSequenceProperties(j.key()));
1683         if (saveHash) {
1684             j.value()->tractor()->set("kdenlive:sequenceproperties.timelineHash", j.value()->timelineHash().toHex().constData());
1685         }
1686     }
1687     return m_documentProperties;
1688 }
1689 
1690 void KdenliveDoc::loadDocumentGuides(const QUuid &uuid, std::shared_ptr<TimelineItemModel> model)
1691 {
1692     const QString guides = getSequenceProperty(uuid, QStringLiteral("guides"));
1693     if (!guides.isEmpty()) {
1694         model->getGuideModel()->importFromJson(guides, true, false);
1695         clearSequenceProperty(uuid, QStringLiteral("guides"));
1696     }
1697 }
1698 
1699 void KdenliveDoc::loadDocumentProperties()
1700 {
1701     QDomNodeList list = m_document.elementsByTagName(QStringLiteral("playlist"));
1702     QDomElement baseElement = m_document.documentElement();
1703     m_documentRoot = baseElement.attribute(QStringLiteral("root"));
1704     if (!m_documentRoot.isEmpty()) {
1705         m_documentRoot = QDir::cleanPath(m_documentRoot) + QLatin1Char('/');
1706     }
1707     QDomElement pl;
1708     for (int i = 0; i < list.count(); i++) {
1709         pl = list.at(i).toElement();
1710         const QString id = pl.attribute(QStringLiteral("id"));
1711         if (id == QLatin1String("main_bin") || id == QLatin1String("main bin")) {
1712             break;
1713         }
1714         pl = QDomElement();
1715     }
1716     if (pl.isNull()) {
1717         qDebug() << "==== DOCUMENT PLAYLIST NOT FOUND!!!!!";
1718         return;
1719     }
1720     QDomNodeList props = pl.elementsByTagName(QStringLiteral("property"));
1721     QString name;
1722     QDomElement e;
1723     for (int i = 0; i < props.count(); i++) {
1724         e = props.at(i).toElement();
1725         name = e.attribute(QStringLiteral("name"));
1726         if (name.startsWith(QLatin1String("kdenlive:docproperties."))) {
1727             name = name.section(QLatin1Char('.'), 1);
1728             if (name == QStringLiteral("storagefolder")) {
1729                 // Make sure we have an absolute path
1730                 QString value = e.firstChild().nodeValue();
1731                 if (QFileInfo(value).isRelative()) {
1732                     value.prepend(m_documentRoot);
1733                 }
1734                 m_documentProperties.insert(name, value);
1735             } else {
1736                 m_documentProperties.insert(name, e.firstChild().nodeValue());
1737                 if (name == QLatin1String("uuid")) {
1738                     m_uuid = QUuid(e.firstChild().nodeValue());
1739                 } else if (name == QLatin1String("timelines")) {
1740                     qDebug() << "=======\n\nFOUND EXTRA TIMELINES:\n\n" << e.firstChild().nodeValue() << "\n\n=========";
1741                 }
1742             }
1743         } else if (name.startsWith(QLatin1String("kdenlive:docmetadata."))) {
1744             name = name.section(QLatin1Char('.'), 1);
1745             m_documentMetadata.insert(name, e.firstChild().nodeValue());
1746         }
1747     }
1748     QString path = m_documentProperties.value(QStringLiteral("storagefolder"));
1749     if (!path.isEmpty()) {
1750         QDir dir(path);
1751         dir.cdUp();
1752         m_projectFolder = dir.absolutePath();
1753         bool ok = false;
1754         // Ensure document storage folder is writable
1755         QString documentId = QDir::cleanPath(m_documentProperties.value(QStringLiteral("documentid")));
1756         documentId.toLongLong(&ok, 10);
1757         if (ok) {
1758             if (!dir.exists(documentId)) {
1759                 if (!dir.mkpath(documentId)) {
1760                     // Invalid storage folder, reset to default
1761                     m_projectFolder.clear();
1762                 }
1763             }
1764         } else {
1765             // Something is wrong, documentid not readable
1766             qDebug() << "=========\n\nCannot read document id: " << documentId << "\n\n==========";
1767         }
1768     }
1769 
1770     QString profile = m_documentProperties.value(QStringLiteral("profile"));
1771     bool profileFound = pCore->setCurrentProfile(profile);
1772     if (!profileFound) {
1773         // try to find matching profile from MLT profile properties
1774         list = m_document.elementsByTagName(QStringLiteral("profile"));
1775         if (!list.isEmpty()) {
1776             std::unique_ptr<ProfileInfo> xmlProfile(new ProfileParam(list.at(0).toElement()));
1777             QString profilePath = ProfileRepository::get()->findMatchingProfile(xmlProfile.get());
1778             // Document profile does not exist, create it as custom profile
1779             if (profilePath.isEmpty()) {
1780                 profilePath = ProfileRepository::get()->saveProfile(xmlProfile.get());
1781             }
1782             profileFound = pCore->setCurrentProfile(profilePath);
1783         }
1784     }
1785     if (!profileFound) {
1786         qDebug() << "ERROR, no matching profile found";
1787     }
1788     updateProjectProfile(false);
1789 }
1790 
1791 void KdenliveDoc::updateProjectProfile(bool reloadProducers, bool reloadThumbs)
1792 {
1793     pCore->taskManager.slotCancelJobs(false, {AbstractTask::PROXYJOB, AbstractTask::AUDIOTHUMBJOB, AbstractTask::TRANSCODEJOB});
1794     double fps = pCore->getCurrentFps();
1795     double fpsChanged = m_timecode.fps() / fps;
1796     m_timecode.setFormat(fps);
1797     if (!reloadProducers) {
1798         return;
1799     }
1800     Q_EMIT updateFps(fpsChanged);
1801     pCore->bin()->reloadAllProducers(reloadThumbs);
1802 }
1803 
1804 void KdenliveDoc::resetProfile(bool reloadThumbs)
1805 {
1806     updateProjectProfile(true, reloadThumbs);
1807     Q_EMIT docModified(true);
1808 }
1809 
1810 void KdenliveDoc::slotSwitchProfile(const QString &profile_path, bool reloadThumbs)
1811 {
1812     // Discard all current jobs except proxy and audio thumbs
1813     pCore->taskManager.slotCancelJobs(false, {AbstractTask::PROXYJOB, AbstractTask::AUDIOTHUMBJOB, AbstractTask::TRANSCODEJOB});
1814     pCore->setCurrentProfile(profile_path);
1815     updateProjectProfile(true, reloadThumbs);
1816     // In case we only have one clip in timeline,
1817     Q_EMIT docModified(true);
1818 }
1819 
1820 void KdenliveDoc::switchProfile(ProfileParam *pf, const QString &clipName)
1821 {
1822     // Request profile update
1823     // Check profile fps so that we don't end up with an fps = 30.003 which would mess things up
1824     QString adjustMessage;
1825     std::unique_ptr<ProfileParam> profile(pf);
1826     double fps = double(profile->frame_rate_num()) / profile->frame_rate_den();
1827     double fps_int;
1828     double fps_frac = std::modf(fps, &fps_int);
1829     if (fps_frac < 0.4) {
1830         profile->m_frame_rate_num = int(fps_int);
1831         profile->m_frame_rate_den = 1;
1832     } else {
1833         // Check for 23.98, 29.97, 59.94
1834         bool fpsFixed = false;
1835         if (qFuzzyCompare(fps_int, 23.0)) {
1836             if (qFuzzyCompare(fps, 23.98) || fps_frac > 0.94) {
1837                 profile->m_frame_rate_num = 24000;
1838                 profile->m_frame_rate_den = 1001;
1839                 fpsFixed = true;
1840             }
1841         } else if (qFuzzyCompare(fps_int, 29.0)) {
1842             if (qFuzzyCompare(fps, 29.97) || fps_frac > 0.94) {
1843                 profile->m_frame_rate_num = 30000;
1844                 profile->m_frame_rate_den = 1001;
1845                 fpsFixed = true;
1846             }
1847         } else if (qFuzzyCompare(fps_int, 59.0)) {
1848             if (qFuzzyCompare(fps, 59.94) || fps_frac > 0.9) {
1849                 profile->m_frame_rate_num = 60000;
1850                 profile->m_frame_rate_den = 1001;
1851                 fpsFixed = true;
1852             }
1853         }
1854         if (!fpsFixed) {
1855             // Unknown profile fps, warn user
1856             profile->m_frame_rate_num = qRound(fps);
1857             profile->m_frame_rate_den = 1;
1858             adjustMessage = i18n("Warning: non standard fps, adjusting to closest integer. ");
1859         }
1860     }
1861     QString matchingProfile = ProfileRepository::get()->findMatchingProfile(profile.get());
1862     if (matchingProfile.isEmpty() && (profile->width() % 2 != 0)) {
1863         // Make sure profile width is a multiple of 8, required by some parts of mlt
1864         profile->adjustDimensions();
1865         matchingProfile = ProfileRepository::get()->findMatchingProfile(profile.get());
1866     }
1867     if (!matchingProfile.isEmpty()) {
1868         // We found a known matching profile, switch and inform user
1869         profile->m_path = matchingProfile;
1870         profile->m_description = ProfileRepository::get()->getProfile(matchingProfile)->description();
1871 
1872         if (KdenliveSettings::default_profile().isEmpty()) {
1873             // Default project format not yet confirmed, propose
1874             QString currentProfileDesc = pCore->getCurrentProfile()->description();
1875             KMessageBox::ButtonCode answer = KMessageBox::questionTwoActionsCancel(
1876                 QApplication::activeWindow(),
1877                 i18n("Your default project profile is %1, but your clip's profile (%2) is %3.\nDo you want to change default profile for future projects?",
1878                      currentProfileDesc, clipName, profile->description()),
1879                 i18n("Change default project profile"), KGuiItem(i18n("Change default to %1", profile->description())),
1880                 KGuiItem(i18n("Keep current default %1", currentProfileDesc)), KGuiItem(i18n("Ask me later")));
1881 
1882             switch (answer) {
1883             case KMessageBox::PrimaryAction:
1884                 // Discard all current jobs
1885                 pCore->taskManager.slotCancelJobs(false, {AbstractTask::PROXYJOB, AbstractTask::AUDIOTHUMBJOB, AbstractTask::TRANSCODEJOB});
1886                 KdenliveSettings::setDefault_profile(profile->path());
1887                 pCore->setCurrentProfile(profile->path());
1888                 updateProjectProfile(true, true);
1889                 Q_EMIT docModified(true);
1890                 return;
1891             case KMessageBox::SecondaryAction:
1892                 return;
1893             default:
1894                 break;
1895             }
1896         }
1897 
1898         // Build actions for the info message (switch / cancel)
1899         const QString profilePath = profile->path();
1900         QAction *ac = new QAction(QIcon::fromTheme(QStringLiteral("dialog-ok")), i18n("Switch"), this);
1901         connect(ac, &QAction::triggered, this, [this, profilePath]() { this->slotSwitchProfile(profilePath, true); });
1902         QAction *ac2 = new QAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("Cancel"), this);
1903         QList<QAction *> list = {ac, ac2};
1904         adjustMessage.append(i18n("Switch to clip (%1) profile %2?", clipName, profile->descriptiveString()));
1905         pCore->displayBinMessage(adjustMessage, KMessageWidget::Information, list, false, BinMessage::BinCategory::ProfileMessage);
1906     } else {
1907         // No known profile, ask user if he wants to use clip profile anyway
1908         if (qFuzzyCompare(double(profile->m_frame_rate_num) / profile->m_frame_rate_den, fps)) {
1909             adjustMessage.append(i18n("\nProfile fps adjusted from original %1", QString::number(fps, 'f', 4)));
1910         } else if (!adjustMessage.isEmpty()) {
1911             adjustMessage.prepend(QLatin1Char('\n'));
1912         }
1913         if (KMessageBox::warningContinueCancel(pCore->window(), i18n("No profile found for your clip %1.\nCreate and switch to new profile (%2x%3, %4fps)?%5",
1914                                                                      clipName, profile->m_width, profile->m_height,
1915                                                                      QString::number(double(profile->m_frame_rate_num) / profile->m_frame_rate_den, 'f', 2),
1916                                                                      adjustMessage)) == KMessageBox::Continue) {
1917             profile->m_description = QStringLiteral("%1x%2 %3fps")
1918                                          .arg(profile->m_width)
1919                                          .arg(profile->m_height)
1920                                          .arg(QString::number(double(profile->m_frame_rate_num) / profile->m_frame_rate_den, 'f', 2));
1921             QString profilePath = ProfileRepository::get()->saveProfile(profile.get());
1922             // Discard all current jobs
1923             pCore->taskManager.slotCancelJobs(false, {AbstractTask::PROXYJOB, AbstractTask::AUDIOTHUMBJOB, AbstractTask::TRANSCODEJOB});
1924             pCore->setCurrentProfile(profilePath);
1925             updateProjectProfile(true, true);
1926             Q_EMIT docModified(true);
1927         }
1928     }
1929 }
1930 
1931 void KdenliveDoc::doAddAction(const QString &name, QAction *a, const QKeySequence &shortcut)
1932 {
1933     pCore->window()->actionCollection()->addAction(name, a);
1934     a->setShortcut(shortcut);
1935     pCore->window()->actionCollection()->setDefaultShortcut(a, a->shortcut());
1936 }
1937 
1938 QAction *KdenliveDoc::getAction(const QString &name)
1939 {
1940     return pCore->window()->actionCollection()->action(name);
1941 }
1942 
1943 void KdenliveDoc::previewProgress(int p)
1944 {
1945     if (pCore->window()) {
1946         Q_EMIT pCore->window()->setPreviewProgress(p);
1947     }
1948 }
1949 
1950 void KdenliveDoc::displayMessage(const QString &text, MessageType type, int timeOut)
1951 {
1952     Q_EMIT pCore->window()->displayMessage(text, type, timeOut);
1953 }
1954 
1955 void KdenliveDoc::selectPreviewProfile()
1956 {
1957     // Read preview profiles and find the best match
1958     if (!KdenliveSettings::previewparams().isEmpty()) {
1959         setDocumentProperty(QStringLiteral("previewparameters"), KdenliveSettings::previewparams());
1960         setDocumentProperty(QStringLiteral("previewextension"), KdenliveSettings::previewextension());
1961         return;
1962     }
1963     KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation);
1964     KConfigGroup group(&conf, "timelinepreview");
1965     QMap<QString, QString> values = group.entryMap();
1966     if (!KdenliveSettings::supportedHWCodecs().isEmpty()) {
1967         QString codecFormat = QStringLiteral("x264-");
1968         codecFormat.append(KdenliveSettings::supportedHWCodecs().first().section(QLatin1Char('_'), 1));
1969         if (values.contains(codecFormat)) {
1970             const QString bestMatch = values.value(codecFormat);
1971             setDocumentProperty(QStringLiteral("previewparameters"), bestMatch.section(QLatin1Char(';'), 0, 0));
1972             setDocumentProperty(QStringLiteral("previewextension"), bestMatch.section(QLatin1Char(';'), 1, 1));
1973             return;
1974         }
1975     }
1976     QMapIterator<QString, QString> i(values);
1977     QStringList matchingProfiles;
1978     QStringList fallBackProfiles;
1979     QSize pSize = pCore->getCurrentFrameDisplaySize();
1980     QString profileSize = QStringLiteral("%1x%2").arg(pSize.width()).arg(pSize.height());
1981 
1982     while (i.hasNext()) {
1983         i.next();
1984         // Check for frame rate
1985         QString params = i.value();
1986         QStringList data = i.value().split(QLatin1Char(' '));
1987         // Check for size mismatch
1988         if (params.contains(QStringLiteral("s="))) {
1989             QString paramSize = params.section(QStringLiteral("s="), 1).section(QLatin1Char(' '), 0, 0);
1990             if (paramSize != profileSize) {
1991                 continue;
1992             }
1993         }
1994         bool rateFound = false;
1995         for (const QString &arg : qAsConst(data)) {
1996             if (arg.startsWith(QStringLiteral("r="))) {
1997                 rateFound = true;
1998                 double fps = arg.section(QLatin1Char('='), 1).toDouble();
1999                 if (fps > 0) {
2000                     if (qAbs(int(pCore->getCurrentFps() * 100) - (fps * 100)) <= 1) {
2001                         matchingProfiles << i.value();
2002                         break;
2003                     }
2004                 }
2005             }
2006         }
2007         if (!rateFound) {
2008             // Profile without fps, can be used as fallBack
2009             fallBackProfiles << i.value();
2010         }
2011     }
2012     QString bestMatch;
2013     if (!matchingProfiles.isEmpty()) {
2014         bestMatch = matchingProfiles.first();
2015     } else if (!fallBackProfiles.isEmpty()) {
2016         bestMatch = fallBackProfiles.first();
2017     }
2018     if (!bestMatch.isEmpty()) {
2019         setDocumentProperty(QStringLiteral("previewparameters"), bestMatch.section(QLatin1Char(';'), 0, 0));
2020         setDocumentProperty(QStringLiteral("previewextension"), bestMatch.section(QLatin1Char(';'), 1, 1));
2021     } else {
2022         setDocumentProperty(QStringLiteral("previewparameters"), QString());
2023         setDocumentProperty(QStringLiteral("previewextension"), QString());
2024     }
2025 }
2026 
2027 QString KdenliveDoc::getAutoProxyProfile()
2028 {
2029     if (m_proxyExtension.isEmpty() || m_proxyParams.isEmpty()) {
2030         initProxySettings();
2031     }
2032     return m_proxyParams;
2033 }
2034 
2035 void KdenliveDoc::initProxySettings()
2036 {
2037     // Read preview profiles and find the best match
2038     KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::AppDataLocation);
2039     KConfigGroup group(&conf, "proxy");
2040     QString params;
2041     QMap<QString, QString> values = group.entryMap();
2042     // Select best proxy profile depending on hw encoder support
2043     if (!KdenliveSettings::supportedHWCodecs().isEmpty()) {
2044         QString codecFormat = QStringLiteral("x264-");
2045         codecFormat.append(KdenliveSettings::supportedHWCodecs().first().section(QLatin1Char('_'), 1));
2046         if (values.contains(codecFormat)) {
2047             params = values.value(codecFormat);
2048         }
2049     }
2050     if (params.isEmpty()) {
2051         params = values.value(QStringLiteral("MJPEG"));
2052     }
2053     m_proxyParams = params.section(QLatin1Char(';'), 0, 0);
2054     m_proxyExtension = params.section(QLatin1Char(';'), 1);
2055 }
2056 
2057 void KdenliveDoc::checkPreviewStack(int ix)
2058 {
2059     // A command was pushed in the middle of the stack, remove all cached data from last undos
2060     Q_EMIT removeInvalidUndo(ix);
2061 }
2062 
2063 void KdenliveDoc::initCacheDirs()
2064 {
2065     bool ok = false;
2066     QString kdenliveCacheDir;
2067     QString documentId = QDir::cleanPath(m_documentProperties.value(QStringLiteral("documentid")));
2068     documentId.toLongLong(&ok, 10);
2069     if (m_projectFolder.isEmpty()) {
2070         kdenliveCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
2071     } else {
2072         kdenliveCacheDir = m_projectFolder;
2073     }
2074     if (!ok || documentId.isEmpty() || kdenliveCacheDir.isEmpty()) {
2075         return;
2076     }
2077     QString basePath = kdenliveCacheDir + QLatin1Char('/') + documentId;
2078     QDir dir(basePath);
2079     dir.mkpath(QStringLiteral("."));
2080     dir.mkdir(QStringLiteral("preview"));
2081     dir.mkdir(QStringLiteral("audiothumbs"));
2082     dir.mkdir(QStringLiteral("videothumbs"));
2083     QDir cacheDir(kdenliveCacheDir);
2084     cacheDir.mkdir(QStringLiteral("proxy"));
2085 }
2086 
2087 const QDir KdenliveDoc::getCacheDir(CacheType type, bool *ok, const QUuid uuid) const
2088 {
2089     QString basePath;
2090     QString kdenliveCacheDir;
2091     QString documentId = QDir::cleanPath(m_documentProperties.value(QStringLiteral("documentid")));
2092     documentId.toLongLong(ok, 10);
2093     if (m_projectFolder.isEmpty()) {
2094         kdenliveCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
2095         if (!*ok || documentId.isEmpty() || kdenliveCacheDir.isEmpty()) {
2096             *ok = false;
2097             return QDir(kdenliveCacheDir);
2098         }
2099     } else {
2100         // Use specified folder to store all files
2101         kdenliveCacheDir = m_projectFolder;
2102     }
2103     basePath = kdenliveCacheDir + QLatin1Char('/') + documentId; // CacheBase
2104     switch (type) {
2105     case SystemCacheRoot:
2106         return QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
2107     case CacheRoot:
2108         basePath = kdenliveCacheDir;
2109         break;
2110     case CachePreview:
2111         basePath.append(QStringLiteral("/preview"));
2112         if (!uuid.isNull() && uuid != m_uuid) {
2113             basePath.append(QStringLiteral("/%1").arg(QString(QCryptographicHash::hash(uuid.toByteArray(), QCryptographicHash::Md5).toHex())));
2114         }
2115         break;
2116     case CacheProxy:
2117         basePath = kdenliveCacheDir;
2118         basePath.append(QStringLiteral("/proxy"));
2119         break;
2120     case CacheAudio:
2121         basePath.append(QStringLiteral("/audiothumbs"));
2122         break;
2123     case CacheThumbs:
2124         basePath.append(QStringLiteral("/videothumbs"));
2125         break;
2126     case CacheTmpWorkFiles:
2127         basePath.append(QStringLiteral("/workfiles"));
2128         break;
2129     case CacheSequence:
2130         basePath.append(QStringLiteral("/sequences"));
2131         break;
2132     default:
2133         break;
2134     }
2135     QDir dir(basePath);
2136     if (!dir.exists()) {
2137         dir.mkpath(QStringLiteral("."));
2138         if (!dir.exists()) {
2139             *ok = false;
2140         }
2141     }
2142     return dir;
2143 }
2144 
2145 QStringList KdenliveDoc::getProxyHashList()
2146 {
2147     return pCore->bin()->getProxyHashList();
2148 }
2149 
2150 std::shared_ptr<TimelineItemModel> KdenliveDoc::getTimeline(const QUuid &uuid, bool allowEmpty)
2151 {
2152     if (m_timelines.contains(uuid)) {
2153         return m_timelines.value(uuid);
2154     }
2155     if (!allowEmpty) {
2156         qDebug() << "REQUESTING UNKNOWN TIMELINE: " << uuid;
2157         Q_ASSERT(false);
2158     }
2159     return nullptr;
2160 }
2161 
2162 QList<QUuid> KdenliveDoc::getTimelinesUuids() const
2163 {
2164     return m_timelines.keys();
2165 }
2166 
2167 QStringList KdenliveDoc::getTimelinesIds()
2168 {
2169     QStringList ids;
2170     QMapIterator<QUuid, std::shared_ptr<TimelineItemModel>> j(m_timelines);
2171     while (j.hasNext()) {
2172         j.next();
2173         ids << QString(j.value()->tractor()->get("id"));
2174     }
2175     return ids;
2176 }
2177 
2178 void KdenliveDoc::addTimeline(const QUuid &uuid, std::shared_ptr<TimelineItemModel> model, bool force)
2179 {
2180     if (force && m_timelines.find(uuid) != m_timelines.end()) {
2181         std::shared_ptr<TimelineItemModel> previousModel = m_timelines.take(uuid);
2182         previousModel.reset();
2183     }
2184     if (m_timelines.find(uuid) != m_timelines.end()) {
2185         qDebug() << "::::: TIMELINE " << uuid << " already inserted in project";
2186         if (m_timelines.value(uuid) != model) {
2187             qDebug() << "::::: TIMELINE INCONSISTENCY";
2188             Q_ASSERT(false);
2189         }
2190         return;
2191     }
2192     if (m_timelines.isEmpty()) {
2193         activeUuid = uuid;
2194     }
2195     m_timelines.insert(uuid, model);
2196 }
2197 
2198 bool KdenliveDoc::checkConsistency()
2199 {
2200     if (m_timelines.isEmpty()) {
2201         qDebug() << "==== CONSISTENCY CHECK FAILED; NO TIMELINE";
2202         return false;
2203     }
2204     QMapIterator<QUuid, std::shared_ptr<TimelineItemModel>> j(m_timelines);
2205     while (j.hasNext()) {
2206         j.next();
2207         if (!j.value()->checkConsistency()) {
2208             return false;
2209         }
2210     }
2211     return true;
2212 }
2213 
2214 void KdenliveDoc::loadSequenceGroupsAndGuides(const QUuid &uuid)
2215 {
2216     Q_ASSERT(m_timelines.find(uuid) != m_timelines.end());
2217     std::shared_ptr<TimelineItemModel> model = m_timelines.value(uuid);
2218     // Load groups
2219     const QString groupsData = getSequenceProperty(uuid, QStringLiteral("groups"));
2220     if (!groupsData.isEmpty()) {
2221         model->loadGroups(groupsData);
2222         clearSequenceProperty(uuid, QStringLiteral("groups"));
2223     }
2224     // Load guides
2225     model->getGuideModel()->loadCategories(guidesCategories(), false);
2226     model->updateFieldOrderFilter(pCore->getCurrentProfile());
2227     loadDocumentGuides(uuid, model);
2228     connect(model.get(), &TimelineModel::saveGuideCategories, this, &KdenliveDoc::saveGuideCategories);
2229 }
2230 
2231 void KdenliveDoc::closeTimeline(const QUuid uuid, bool onDeletion)
2232 {
2233     Q_ASSERT(m_timelines.find(uuid) != m_timelines.end());
2234     // Sync all sequence properties
2235     if (onDeletion) {
2236         auto model = m_timelines.take(uuid);
2237         model->prepareClose(!closing);
2238         model.reset();
2239     } else {
2240         auto model = m_timelines.value(uuid);
2241         if (!closing) {
2242             setSequenceProperty(uuid, QStringLiteral("groups"), model->groupsData());
2243             model->passSequenceProperties(getSequenceProperties(uuid));
2244         }
2245         model->isClosed = true;
2246     }
2247     // Clear all sequence properties
2248     m_sequenceProperties.remove(uuid);
2249 }
2250 
2251 void KdenliveDoc::storeGroups(const QUuid &uuid)
2252 {
2253     Q_ASSERT(m_timelines.find(uuid) != m_timelines.end());
2254     setSequenceProperty(uuid, QStringLiteral("groups"), m_timelines.value(uuid)->groupsData());
2255     m_timelines.value(uuid)->passSequenceProperties(getSequenceProperties(uuid));
2256 }
2257 
2258 void KdenliveDoc::checkUsage(const QUuid &uuid)
2259 {
2260     Q_ASSERT(m_timelines.find(uuid) != m_timelines.end());
2261     qDebug() << "===== CHECKING USAGE FOR: " << uuid << " = " << m_timelines.value(uuid).use_count();
2262 }
2263 
2264 std::shared_ptr<MarkerSortModel> KdenliveDoc::getFilteredGuideModel(const QUuid uuid)
2265 {
2266     Q_ASSERT(m_timelines.find(uuid) != m_timelines.end());
2267     return m_timelines.value(uuid)->getFilteredGuideModel();
2268 }
2269 
2270 std::shared_ptr<MarkerListModel> KdenliveDoc::getGuideModel(const QUuid uuid) const
2271 {
2272     Q_ASSERT(m_timelines.find(uuid) != m_timelines.end());
2273     return m_timelines.value(uuid)->getGuideModel();
2274 }
2275 
2276 int KdenliveDoc::openedTimelineCount() const
2277 {
2278     return m_timelines.size();
2279 }
2280 
2281 const QStringList KdenliveDoc::getSecondaryTimelines() const
2282 {
2283     QString timelines = getDocumentProperty(QStringLiteral("timelines"));
2284     if (timelines.isEmpty()) {
2285         return QStringList();
2286     }
2287     return getDocumentProperty(QStringLiteral("timelines")).split(QLatin1Char(';'));
2288 }
2289 
2290 const QString KdenliveDoc::projectName() const
2291 {
2292     if (!m_url.isValid()) {
2293         return i18n("Untitled");
2294     }
2295     return m_url.fileName();
2296 }
2297 
2298 const QString KdenliveDoc::documentRoot() const
2299 {
2300     return m_documentRoot;
2301 }
2302 
2303 bool KdenliveDoc::updatePreviewSettings(const QString &profile)
2304 {
2305     if (profile.isEmpty()) {
2306         return false;
2307     }
2308     QString params = profile.section(QLatin1Char(';'), 0, 0);
2309     QString ext = profile.section(QLatin1Char(';'), 1, 1);
2310     if (params != getDocumentProperty(QStringLiteral("previewparameters")) || ext != getDocumentProperty(QStringLiteral("previewextension"))) {
2311         // Timeline preview params changed, delete all existing previews.
2312         setDocumentProperty(QStringLiteral("previewparameters"), params);
2313         setDocumentProperty(QStringLiteral("previewextension"), ext);
2314         return true;
2315     }
2316     return false;
2317 }
2318 
2319 QMap<int, QStringList> KdenliveDoc::getProjectTags() const
2320 {
2321     QMap<int, QStringList> tags;
2322     int ix = 1;
2323     for (int i = 1; i < 50; i++) {
2324         QString current = getDocumentProperty(QString("tag%1").arg(i));
2325         if (current.isEmpty()) {
2326             break;
2327         }
2328         tags.insert(ix, {QString::number(ix), current.section(QLatin1Char(':'), 0, 0), current.section(QLatin1Char(':'), 1)});
2329         ix++;
2330     }
2331     if (tags.isEmpty()) {
2332         tags.insert(1, {QStringLiteral("1"), QStringLiteral("#ff0000"), i18n("Red")});
2333         tags.insert(2, {QStringLiteral("2"), QStringLiteral("#00ff00"), i18n("Green")});
2334         tags.insert(3, {QStringLiteral("3"), QStringLiteral("#0000ff"), i18n("Blue")});
2335         tags.insert(4, {QStringLiteral("4"), QStringLiteral("#ffff00"), i18n("Yellow")});
2336         tags.insert(5, {QStringLiteral("5"), QStringLiteral("#00ffff"), i18n("Cyan")});
2337     }
2338     return tags;
2339 }
2340 
2341 int KdenliveDoc::audioChannels() const
2342 {
2343     return getDocumentProperty(QStringLiteral("audioChannels"), QStringLiteral("2")).toInt();
2344 }
2345 
2346 QString &KdenliveDoc::modifiedDecimalPoint()
2347 {
2348     return m_modifiedDecimalPoint;
2349 }
2350 
2351 const QString KdenliveDoc::subTitlePath(const QUuid &uuid, int ix, bool final)
2352 {
2353     QString documentId = QDir::cleanPath(m_documentProperties.value(QStringLiteral("documentid")));
2354     QString path = (m_url.isValid() && final) ? m_url.fileName() : documentId;
2355     if (uuid != m_uuid) {
2356         path.append(uuid.toString());
2357     }
2358     if (ix > 0) {
2359         path.append(QStringLiteral("-%1").arg(ix));
2360     }
2361     if (m_url.isValid() && final) {
2362         return QFileInfo(m_url.toLocalFile()).dir().absoluteFilePath(QString("%1.srt").arg(path));
2363     } else {
2364         return QDir::temp().absoluteFilePath(QString("%1.srt").arg(path));
2365     }
2366 }
2367 
2368 QMap<std::pair<int, QString>, QString> KdenliveDoc::multiSubtitlePath(const QUuid &uuid)
2369 {
2370     QMap<std::pair<int, QString>, QString> results;
2371     const QString data = getSequenceProperty(uuid, QStringLiteral("subtitlesList"));
2372     auto json = QJsonDocument::fromJson(data.toUtf8());
2373     if (!json.isArray()) {
2374         qDebug() << "Error : Json file should be an array";
2375         return results;
2376     }
2377     auto list = json.array();
2378     for (const auto &entry : qAsConst(list)) {
2379         if (!entry.isObject()) {
2380             qDebug() << "Warning : Skipping invalid subtitle data";
2381             continue;
2382         }
2383         auto entryObj = entry.toObject();
2384         if (!entryObj.contains(QLatin1String("name")) || !entryObj.contains(QLatin1String("file"))) {
2385             qDebug() << "Warning : Skipping invalid subtitle data (does not have a name or file)";
2386             continue;
2387         }
2388         const QString subName = entryObj[QLatin1String("name")].toString();
2389         int subId = entryObj[QLatin1String("id")].toInt();
2390         QString subUrl = entryObj[QLatin1String("file")].toString();
2391         if (QFileInfo(subUrl).isRelative()) {
2392             subUrl.prepend(m_documentRoot);
2393         }
2394         results.insert({subId, subName}, subUrl);
2395     }
2396     return results;
2397 }
2398 
2399 bool KdenliveDoc::hasSubtitles() const
2400 {
2401     QMapIterator<QUuid, std::shared_ptr<TimelineItemModel>> j(m_timelines);
2402     while (j.hasNext()) {
2403         j.next();
2404         if (j.value()->hasSubtitleModel()) {
2405             return true;
2406         }
2407     }
2408     return false;
2409 }
2410 
2411 void KdenliveDoc::generateRenderSubtitleFile(const QUuid &uuid, int in, int out, const QString &subtitleFile)
2412 {
2413     if (m_timelines.contains(uuid)) {
2414         m_timelines.value(uuid)->getSubtitleModel()->subtitleFileFromZone(in, out, subtitleFile);
2415     }
2416 }
2417 
2418 // static
2419 void KdenliveDoc::useOriginals(QDomDocument &doc)
2420 {
2421     QString root = doc.documentElement().attribute(QStringLiteral("root"));
2422     if (!root.isEmpty() && !root.endsWith(QLatin1Char('/'))) {
2423         root.append(QLatin1Char('/'));
2424     }
2425 
2426     // replace proxy clips with originals
2427     QMap<QString, QString> proxies = pCore->projectItemModel()->getProxies(root);
2428     QDomNodeList producers = doc.elementsByTagName(QStringLiteral("producer"));
2429     QDomNodeList chains = doc.elementsByTagName(QStringLiteral("chain"));
2430     processProxyNodes(producers, root, proxies);
2431     processProxyNodes(chains, root, proxies);
2432 }
2433 
2434 // static
2435 void KdenliveDoc::disableSubtitles(QDomDocument &doc)
2436 {
2437     QDomNodeList filters = doc.elementsByTagName(QStringLiteral("filter"));
2438     for (int i = 0; i < filters.length(); ++i) {
2439         auto filter = filters.at(i).toElement();
2440         if (Xml::getXmlProperty(filter, QStringLiteral("mlt_service")) == QLatin1String("avfilter.subtitles")) {
2441             Xml::setXmlProperty(filter, QStringLiteral("disable"), QStringLiteral("1"));
2442         }
2443     }
2444 }
2445 
2446 void KdenliveDoc::makeBackgroundTrackTransparent(QDomDocument &doc)
2447 {
2448     QDomNodeList prods = doc.elementsByTagName(QStringLiteral("producer"));
2449     // Switch all black track producers to transparent
2450     for (int i = 0; i < prods.length(); ++i) {
2451         auto prod = prods.at(i).toElement();
2452         if (Xml::getXmlProperty(prod, QStringLiteral("kdenlive:playlistid")) == QStringLiteral("black_track")) {
2453             Xml::setXmlProperty(prod, QStringLiteral("resource"), QStringLiteral("0"));
2454         }
2455     }
2456 }
2457 
2458 void KdenliveDoc::setAutoclosePlaylists(QDomDocument &doc, const QString &mainSequenceUuid)
2459 {
2460     // We should only set the autoclose attribute on the main sequence playlists.
2461     // Otherwise if a sequence is reused several times, its playback will be broken
2462     QDomNodeList playlists = doc.elementsByTagName(QStringLiteral("playlist"));
2463     QDomNodeList tractors = doc.elementsByTagName(QStringLiteral("tractor"));
2464     QStringList matches;
2465     for (int i = 0; i < tractors.length(); ++i) {
2466         if (tractors.at(i).toElement().attribute(QStringLiteral("id")) == mainSequenceUuid) {
2467             // We found the main sequence tractor, list its tracks
2468             QDomNodeList tracks = tractors.at(i).toElement().elementsByTagName(QStringLiteral("track"));
2469             for (int j = 0; j < tracks.length(); ++j) {
2470                 matches << tracks.at(j).toElement().attribute(QStringLiteral("producer"));
2471             }
2472             break;
2473         }
2474     }
2475     for (int i = 0; i < playlists.length(); ++i) {
2476         auto playlist = playlists.at(i).toElement();
2477         if (matches.contains(playlist.attribute(QStringLiteral("id")))) {
2478             playlist.setAttribute(QStringLiteral("autoclose"), 1);
2479         }
2480     }
2481 }
2482 
2483 void KdenliveDoc::processProxyNodes(QDomNodeList producers, const QString &root, const QMap<QString, QString> &proxies)
2484 {
2485 
2486     QString producerResource;
2487     QString producerService;
2488     QString originalProducerService;
2489     QString suffix;
2490     QString prefix;
2491     for (int n = 0; n < producers.length(); ++n) {
2492         QDomElement e = producers.item(n).toElement();
2493         producerResource = Xml::getXmlProperty(e, QStringLiteral("resource"));
2494         producerService = Xml::getXmlProperty(e, QStringLiteral("mlt_service"));
2495         originalProducerService = Xml::getXmlProperty(e, QStringLiteral("kdenlive:original.mlt_service"));
2496         if (producerResource.isEmpty() || producerService == QLatin1String("color")) {
2497             continue;
2498         }
2499         if (producerService == QLatin1String("timewarp")) {
2500             // slowmotion producer
2501             prefix = producerResource.section(QLatin1Char(':'), 0, 0) + QLatin1Char(':');
2502             producerResource = producerResource.section(QLatin1Char(':'), 1);
2503         } else {
2504             prefix.clear();
2505         }
2506         if (producerService == QLatin1String("framebuffer")) {
2507             // slowmotion producer
2508             suffix = QLatin1Char('?') + producerResource.section(QLatin1Char('?'), 1);
2509             producerResource = producerResource.section(QLatin1Char('?'), 0, 0);
2510         } else {
2511             suffix.clear();
2512         }
2513         if (!producerResource.isEmpty()) {
2514             if (QFileInfo(producerResource).isRelative()) {
2515                 producerResource.prepend(root);
2516             }
2517             if (proxies.contains(producerResource)) {
2518                 if (!originalProducerService.isEmpty() && originalProducerService != producerService) {
2519                     // Proxy clips can sometimes use a different mlt service, for example playlists (xml) will use avformat. Fix
2520                     Xml::setXmlProperty(e, QStringLiteral("mlt_service"), originalProducerService);
2521                 }
2522                 QString replacementResource = proxies.value(producerResource);
2523                 Xml::setXmlProperty(e, QStringLiteral("resource"), prefix + replacementResource + suffix);
2524                 if (producerService == QLatin1String("timewarp")) {
2525                     Xml::setXmlProperty(e, QStringLiteral("warp_resource"), replacementResource);
2526                 }
2527                 // We need to delete the "aspect_ratio" property because proxy clips
2528                 // sometimes have different ratio than original clips
2529                 Xml::removeXmlProperty(e, QStringLiteral("aspect_ratio"));
2530                 Xml::removeMetaProperties(e);
2531             }
2532         }
2533     }
2534 }
2535 
2536 void KdenliveDoc::cleanupTimelinePreview(const QDateTime &documentDate)
2537 {
2538     if (m_url.isEmpty()) {
2539         // Document was never saved, nothing to do
2540         return;
2541     }
2542     bool ok;
2543     QDir cacheDir = getCacheDir(CachePreview, &ok);
2544     if (cacheDir.exists() && cacheDir.dirName() == QLatin1String("preview") && ok) {
2545         QFileInfoList chunksList = cacheDir.entryInfoList(QDir::Files, QDir::Time);
2546         for (auto &chunkFile : chunksList) {
2547             if (chunkFile.lastModified() > documentDate) {
2548                 // This chunk is invalid
2549                 QString chunkName = chunkFile.fileName().section(QLatin1Char('.'), 0, 0);
2550                 bool ok;
2551                 chunkName.toInt(&ok);
2552                 if (!ok) {
2553                     // This is not one of our chunks
2554                     continue;
2555                 }
2556                 // Physically remove chunk file
2557                 cacheDir.remove(chunkFile.fileName());
2558             } else {
2559                 // Done
2560                 break;
2561             }
2562         }
2563         // Check secondary timelines preview folders
2564         QFileInfoList dirsList = cacheDir.entryInfoList(QDir::AllDirs, QDir::Time);
2565         for (auto &dir : dirsList) {
2566             QDir sourceDir(dir.absolutePath());
2567             if (!sourceDir.absolutePath().contains(QLatin1String("preview"))) {
2568                 continue;
2569             }
2570             QFileInfoList chunksList = sourceDir.entryInfoList(QDir::Files, QDir::Time);
2571             for (auto &chunkFile : chunksList) {
2572                 if (chunkFile.lastModified() > documentDate) {
2573                     // This chunk is invalid
2574                     QString chunkName = chunkFile.fileName().section(QLatin1Char('.'), 0, 0);
2575                     bool ok;
2576                     chunkName.toInt(&ok);
2577                     if (!ok) {
2578                         // This is not one of our chunks
2579                         continue;
2580                     }
2581                     // Physically remove chunk file
2582                     sourceDir.remove(chunkFile.fileName());
2583                 } else {
2584                     // Done
2585                     break;
2586                 }
2587             }
2588         }
2589     }
2590 }
2591 
2592 // static
2593 const QStringList KdenliveDoc::getDefaultGuideCategories()
2594 {
2595     // Don't change this or it will break compatibility for projects created with Kdenlive < 22.12
2596     QStringList colors = {QLatin1String("#9b59b6"), QLatin1String("#3daee9"), QLatin1String("#1abc9c"), QLatin1String("#1cdc9a"), QLatin1String("#c9ce3b"),
2597                           QLatin1String("#fdbc4b"), QLatin1String("#f39c1f"), QLatin1String("#f47750"), QLatin1String("#da4453")};
2598     QStringList guidesCategories;
2599     for (int i = 0; i < 9; i++) {
2600         guidesCategories << QString("%1 %2:%3:%4").arg(i18n("Category")).arg(QString::number(i + 1)).arg(QString::number(i)).arg(colors.at(i));
2601     }
2602     return guidesCategories;
2603 }
2604 
2605 const QUuid KdenliveDoc::uuid() const
2606 {
2607     return m_uuid;
2608 }
2609 
2610 void KdenliveDoc::loadSequenceProperties(const QUuid &uuid, Mlt::Properties sequenceProps)
2611 {
2612     QMap<QString, QString> sequenceProperties = m_sequenceProperties.take(uuid);
2613     for (int i = 0; i < sequenceProps.count(); i++) {
2614         sequenceProperties.insert(qstrdup(sequenceProps.get_name(i)), qstrdup(sequenceProps.get(i)));
2615     }
2616     m_sequenceProperties.insert(uuid, sequenceProperties);
2617 }