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 }