File indexing completed on 2024-05-12 04:54:04

0001 /*
0002    SPDX-FileCopyrightText: 2017 Nicolas Carion
0003    SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 #include "meltBuilder.hpp"
0007 #include "../clipmodel.hpp"
0008 #include "../timelineitemmodel.hpp"
0009 #include "../timelinemodel.hpp"
0010 #include "../trackmodel.hpp"
0011 #include "../undohelper.hpp"
0012 #include "bin/bin.h"
0013 #include "bin/projectclip.h"
0014 #include "bin/projectitemmodel.h"
0015 #include "core.h"
0016 #include "kdenlivesettings.h"
0017 #include "transitions/transitionsrepository.hpp"
0018 
0019 #include <KLocalizedString>
0020 #include <KMessageBox>
0021 #include <QApplication>
0022 #include <QDebug>
0023 #include <QSet>
0024 #include <mlt++/MltField.h>
0025 #include <mlt++/MltMultitrack.h>
0026 #include <mlt++/MltProfile.h>
0027 #include <mlt++/MltService.h>
0028 #include <mlt++/MltTractor.h>
0029 #include <mlt++/MltTransition.h>
0030 #include <project/projectmanager.h>
0031 
0032 static QStringList m_errorMessage;
0033 static QStringList m_notesLog;
0034 std::unordered_map<QString, QString> binIdCorresp;
0035 
0036 bool constructTrackFromMelt(const std::shared_ptr<TimelineItemModel> &timeline, int tid, bool useMappedIds, const QString trackTag, Mlt::Tractor &track,
0037                             Fun &undo, Fun &redo, bool audioTrack, const QString &originalDecimalPoint);
0038 bool constructTrackFromMelt(const std::shared_ptr<TimelineItemModel> &timeline, int tid, bool useMappedIds, const QString trackTag, Mlt::Playlist &track,
0039                             Fun &undo, Fun &redo, bool audioTrack, const QString &originalDecimalPoint, int playlist,
0040                             const QList<Mlt::Transition *> &compositions);
0041 
0042 bool loadProjectBin(Mlt::Tractor tractor, const QUuid &activeUuid)
0043 {
0044     Fun undo = []() { return true; };
0045     Fun redo = []() { return true; };
0046     // First, we destruct the previous tracks
0047     QStringList expandedFolders;
0048     int zoomLevel = -1;
0049     binIdCorresp.clear();
0050     m_notesLog.clear();
0051     QList<QUuid> brokenSequences = pCore->projectItemModel()->loadBinPlaylist(&tractor, binIdCorresp, expandedFolders, activeUuid, zoomLevel);
0052     if (!brokenSequences.isEmpty()) {
0053         KMessageBox::error(qApp->activeWindow(), i18n("Found an invalid sequence clip in Bin"));
0054         return false;
0055     }
0056     QStringList foldersToExpand;
0057     // Find updated ids for expanded folders
0058     for (const QString &folderId : expandedFolders) {
0059         if (binIdCorresp.count(folderId) > 0) {
0060             foldersToExpand << binIdCorresp.at(folderId);
0061         }
0062     }
0063     if (pCore->window()) {
0064         pCore->bin()->checkMissingProxies();
0065         pCore->bin()->loadBinProperties(foldersToExpand, zoomLevel);
0066     }
0067     return true;
0068 }
0069 
0070 bool constructTimelineFromTractor(const std::shared_ptr<TimelineItemModel> &timeline, const std::shared_ptr<ProjectItemModel> &projectModel,
0071                                   Mlt::Tractor tractor, const QString &originalDecimalPoint, const QString &chunks, const QString &dirty, bool enablePreview)
0072 {
0073     Fun undo = []() { return true; };
0074     Fun redo = []() { return true; };
0075     // First, we destruct the previous tracks
0076     timeline->requestReset(undo, redo);
0077     m_errorMessage.clear();
0078     bool useMappedIds = true;
0079 
0080     QStringList expandedFolders;
0081     if (projectModel) {
0082         // This is an old format project file
0083         int zoomLevel = -1;
0084         if (timeline->uuid() == pCore->currentTimelineId()) {
0085             binIdCorresp.clear();
0086             projectModel->loadBinPlaylist(&tractor, binIdCorresp, expandedFolders, timeline->uuid(), zoomLevel);
0087         } else {
0088             projectModel->loadTractorPlaylist(tractor, binIdCorresp);
0089         }
0090         QStringList foldersToExpand;
0091         // Find updated ids for expanded folders
0092         for (const QString &folderId : expandedFolders) {
0093             if (binIdCorresp.count(folderId) > 0) {
0094                 foldersToExpand << binIdCorresp.at(folderId);
0095             }
0096         }
0097         if (pCore->window()) {
0098             pCore->bin()->checkMissingProxies();
0099             pCore->bin()->loadBinProperties(foldersToExpand, zoomLevel);
0100         }
0101     } else {
0102         // loading an extra timeline
0103         if (tractor.property_exists("_dontmapids")) {
0104             // We are reopening a closed sequence, don't use mapped ids!
0105             useMappedIds = false;
0106         }
0107     }
0108 
0109     QSet<QString> reserved_names{QLatin1String("playlistmain"), QLatin1String("timeline_preview"), QLatin1String("timeline_overlay"),
0110                                  QLatin1String("black_track"), QLatin1String("overlay_track")};
0111     bool ok = true;
0112 
0113     // Import master track effects
0114     std::shared_ptr<Mlt::Service> serv = std::make_shared<Mlt::Service>(tractor.get_service());
0115     timeline->importMasterEffects(serv);
0116 
0117     QList<int> videoTracksIndexes;
0118     QList<int> lockedTracksIndexes;
0119     int vTracks = 0;
0120     int aTracks = 0;
0121     int aTracksCount = 1;
0122     // Black track index
0123     videoTracksIndexes << 0;
0124     QString playlist_name;
0125     for (int i = 0; i < tractor.count() && ok; i++) {
0126         std::unique_ptr<Mlt::Producer> track(tractor.track(i));
0127         if (track->property_exists("kdenlive:playlistid")) {
0128             playlist_name = track->get("kdenlive:playlistid");
0129         } else {
0130             playlist_name = track->get("id");
0131         }
0132         if (reserved_names.contains(playlist_name)) {
0133             continue;
0134         }
0135         switch (track->type()) {
0136         case mlt_service_tractor_type: {
0137             // that is a double track
0138             if (track->get_int("kdenlive:audio_track") == 1) {
0139                 aTracksCount++;
0140             }
0141             break;
0142         }
0143         case mlt_service_playlist_type: {
0144             // that is a single track
0145             Mlt::Playlist local_playlist(*track);
0146             if (local_playlist.get_int("kdenlive:audio_track") == 1) {
0147                 aTracksCount++;
0148             }
0149             break;
0150         }
0151         default:
0152             break;
0153         }
0154     }
0155 
0156     qDebug() << "=== OPENING FILE WITH TRACKS: " << tractor.count();
0157     for (int i = 0; i < tractor.count() && ok; i++) {
0158         qApp->processEvents();
0159         std::unique_ptr<Mlt::Producer> track(tractor.track(i));
0160         if (track->property_exists("kdenlive:playlistid")) {
0161             playlist_name = track->get("kdenlive:playlistid");
0162         } else {
0163             playlist_name = track->get("id");
0164         }
0165         if (reserved_names.contains(playlist_name)) {
0166             if (playlist_name == QLatin1String("timeline_preview")) {
0167                 Mlt::Playlist local_playlist(*track);
0168                 pCore->loadTimelinePreview(timeline->uuid(), chunks, dirty, enablePreview, local_playlist);
0169             }
0170             continue;
0171         }
0172         switch (track->type()) {
0173         case mlt_service_producer_type:
0174             // TODO check that it is the black track, and otherwise log an error
0175             break;
0176         case mlt_service_tractor_type: {
0177             // that is a double track
0178             int tid;
0179             Mlt::Tractor local_tractor(*track.get());
0180             qDebug() << ":::: FOUND TRACTOR WITH TRACKS: " << local_tractor.count() << "\n\n___________________________";
0181             if (local_tractor.count() == 0) {
0182                 // Invalid track
0183                 break;
0184             }
0185             bool audioTrack = track->get_int("kdenlive:audio_track") == 1;
0186             if (!audioTrack) {
0187                 videoTracksIndexes << i;
0188                 vTracks++;
0189             } else {
0190                 aTracks++;
0191             }
0192             ok = timeline->requestTrackInsertion(-1, tid, QString(), audioTrack, undo, redo, false);
0193             if (track->get_int("kdenlive:locked_track") > 0) {
0194                 lockedTracksIndexes << tid;
0195             }
0196             const QString trackTag = audioTrack ? QStringLiteral("A%1").arg(aTracksCount - aTracks) : QStringLiteral("V%1").arg(vTracks);
0197             ok = ok && constructTrackFromMelt(timeline, tid, useMappedIds, trackTag, local_tractor, undo, redo, audioTrack, originalDecimalPoint);
0198             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), track->get("kdenlive:thumbs_format"));
0199             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), track->get("kdenlive:audio_rec"));
0200             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:timeline_active"), track->get("kdenlive:timeline_active"));
0201             break;
0202         }
0203         case mlt_service_playlist_type: {
0204             // that is a single track
0205             int tid;
0206             qDebug() << "=== LOADING PLAYLIST FROM SINGLE TRACK\n00000000000000000000000";
0207             Mlt::Playlist local_playlist(*track);
0208             const QString trackName = local_playlist.get("kdenlive:track_name");
0209             bool audioTrack = local_playlist.get_int("kdenlive:audio_track") == 1;
0210             if (!audioTrack) {
0211                 videoTracksIndexes << i;
0212             }
0213             ok = timeline->requestTrackInsertion(-1, tid, trackName, audioTrack, undo, redo, false);
0214             int muteState = track->get_int("hide");
0215             if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) {
0216                 timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState));
0217             }
0218             const QString trackTag = audioTrack ? QStringLiteral("A%1").arg(aTracksCount - aTracks) : QStringLiteral("V%1").arg(vTracks);
0219             ok = ok && constructTrackFromMelt(timeline, tid, useMappedIds, trackTag, local_playlist, undo, redo, audioTrack, originalDecimalPoint, 0,
0220                                               QList<Mlt::Transition *>());
0221             if (local_playlist.get_int("kdenlive:locked_track") > 0) {
0222                 lockedTracksIndexes << tid;
0223             }
0224             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), local_playlist.get("kdenlive:thumbs_format"));
0225             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), track->get("kdenlive:audio_rec"));
0226             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:timeline_active"), track->get("kdenlive:timeline_active"));
0227             break;
0228         }
0229         default:
0230             qWarning() << "Unexpected track type" << track->type();
0231         }
0232     }
0233     timeline->_resetView();
0234 
0235     // Loading compositions
0236     Mlt::Service *prod = tractor.producer();
0237     QList<Mlt::Transition *> compositions;
0238     if (prod && prod->is_valid()) {
0239         QScopedPointer<Mlt::Service> service(prod);
0240         while ((service != nullptr) && service->is_valid()) {
0241             if (service->type() == mlt_service_transition_type) {
0242                 Mlt::Transition t(mlt_transition(service->get_service()));
0243                 if (t.get_b_track() >= timeline->tractor()->count()) {
0244                     // Composition outside of available track, maybe because of a preview track
0245                     service.reset(service->producer());
0246                     continue;
0247                 }
0248                 QString id(t.get("kdenlive_id"));
0249                 QString internal(t.get("internal_added"));
0250                 QString isMix(t.get("kdenlive:mixcut"));
0251                 if (internal.isEmpty() && isMix.isEmpty()) {
0252                     compositions << new Mlt::Transition(t);
0253                     if (id.isEmpty()) {
0254                         qWarning() << "transition without id" << t.get("id") << t.get("mlt_service") << "on track" << t.get_b_track();
0255                         t.set("kdenlive_id", t.get("mlt_service"));
0256                     }
0257                 }
0258             }
0259             service.reset(service->producer());
0260         }
0261     }
0262     // Sort compositions and insert
0263     bool compositionOk = true;
0264     QStringList validCompositions;
0265     while (!compositions.isEmpty()) {
0266         QScopedPointer<Mlt::Transition> t(compositions.takeFirst());
0267         QString id(t->get("kdenlive_id"));
0268         int compoId;
0269         int aTrack = t->get_a_track();
0270         if (aTrack > tractor.count()) {
0271             m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3, compositing with track %4.", t->get("id"), t->get_b_track(), t->get_in(),
0272                                    t->get_a_track());
0273             continue;
0274         }
0275         if (t->get_int("force_track") == 0) {
0276             // This is an automatic composition, check that we composite with lower track or warn
0277             int pos = videoTracksIndexes.indexOf(t->get_b_track());
0278             if (pos > 0 && videoTracksIndexes.at(pos - 1) != aTrack) {
0279                 t->set("force_track", 1);
0280                 m_errorMessage << i18n("Incorrect composition %1 found on track %2 at %3, compositing with track %4 was set to forced track.", t->get("id"),
0281                                        t->get_b_track(), t->get_in(), t->get_a_track());
0282             }
0283         }
0284         if (!validCompositions.contains(id) && !TransitionsRepository::get()->exists(id)) {
0285             m_errorMessage << i18n("Unknown composition %1 found on track %2 at %3, compositing with track %4.", id, t->get_b_track(), t->get_in(),
0286                                    t->get_a_track());
0287             continue;
0288         }
0289         validCompositions << id;
0290         auto transProps = std::make_unique<Mlt::Properties>(t->get_properties());
0291         compositionOk = timeline->requestCompositionInsertion(id, timeline->getTrackIndexFromPosition(t->get_b_track() - 1), t->get_a_track(), t->get_in(),
0292                                                               t->get_length(), std::move(transProps), compoId, undo, redo, false, originalDecimalPoint);
0293         if (!compositionOk) {
0294             // timeline->requestItemDeletion(compoId, false);
0295             m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3.", t->get("id"), t->get_b_track(), t->get_in());
0296             continue;
0297         }
0298     }
0299 
0300     // build internal track compositing
0301     timeline->buildTrackCompositing();
0302 
0303     // load locked state as last step
0304     for (int tid : qAsConst(lockedTracksIndexes)) {
0305         timeline->setTrackLockedState(tid, true);
0306     }
0307 
0308     if (!ok) {
0309         // TODO log error
0310         // Don't abort loading because of failed composition
0311         undo();
0312         return false;
0313     }
0314     timeline->isLoading = false;
0315     if (!m_errorMessage.isEmpty()) {
0316         if (pCore->window()) {
0317             KMessageBox::error(qApp->activeWindow(), m_errorMessage.join("\n"), i18n("Problems found in your project file"));
0318         } else {
0319             qWarning() << "++++++++++++++++\n++++++++++++++++\n\n" << m_errorMessage.join("\n") << "\n\n++++++++++++++++\n++++++++++++++++";
0320         }
0321     }
0322     return true;
0323 }
0324 
0325 bool constructTimelineFromMelt(const std::shared_ptr<TimelineItemModel> &timeline, Mlt::Tractor tractor, const QString &originalDecimalPoint,
0326                                const QString &chunks, const QString &dirty, bool enablePreview, bool *projectErrors)
0327 {
0328     if (tractor.count() == 0) {
0329         // Trying to load invalid tractor, abort
0330         return false;
0331     }
0332     Fun undo = []() { return true; };
0333     Fun redo = []() { return true; };
0334     // First, we destruct the previous tracks
0335     timeline->requestReset(undo, redo);
0336     m_errorMessage.clear();
0337     QStringList expandedFolders;
0338     int zoomLevel = -1;
0339     if (timeline->uuid() == pCore->currentTimelineId()) {
0340         pCore->projectItemModel()->loadBinPlaylist(&tractor, binIdCorresp, expandedFolders, timeline->uuid(), zoomLevel);
0341     }
0342     QStringList foldersToExpand;
0343     // Find updated ids for expanded folders
0344     for (const QString &folderId : expandedFolders) {
0345         if (binIdCorresp.count(folderId) > 0) {
0346             foldersToExpand << binIdCorresp.at(folderId);
0347         }
0348     }
0349     if (pCore->window()) {
0350         pCore->bin()->checkMissingProxies();
0351         pCore->bin()->loadBinProperties(foldersToExpand, zoomLevel);
0352     }
0353 
0354     QSet<QString> reserved_names{QLatin1String("playlistmain"), QLatin1String("timeline_preview"), QLatin1String("timeline_overlay"),
0355                                  QLatin1String("black_track"), QLatin1String("overlay_track")};
0356     bool ok = true;
0357 
0358     // Import master track effects
0359     std::shared_ptr<Mlt::Service> serv = std::make_shared<Mlt::Service>(tractor.get_service());
0360     timeline->importMasterEffects(serv);
0361 
0362     QList<int> videoTracksIndexes;
0363     QList<int> lockedTracksIndexes;
0364     // Black track index
0365     videoTracksIndexes << 0;
0366     // Check how many audio/video tracks we have
0367     int vTracks = 0;
0368     int aTracks = 0;
0369     int aTracksCount = 1;
0370     QString playlist_name;
0371     for (int i = 0; i < tractor.count() && ok; i++) {
0372         std::unique_ptr<Mlt::Producer> track(tractor.track(i));
0373         if (track->property_exists("kdenlive:playlistid")) {
0374             playlist_name = track->get("kdenlive:playlistid");
0375         } else {
0376             playlist_name = track->get("id");
0377         }
0378         if (reserved_names.contains(playlist_name)) {
0379             continue;
0380         }
0381         switch (track->type()) {
0382         case mlt_service_tractor_type: {
0383             // that is a double track
0384             if (track->get_int("kdenlive:audio_track") == 1) {
0385                 aTracksCount++;
0386             }
0387             break;
0388         }
0389         case mlt_service_playlist_type: {
0390             // that is a single track
0391             Mlt::Playlist local_playlist(*track);
0392             if (local_playlist.get_int("kdenlive:audio_track") == 1) {
0393                 aTracksCount++;
0394             }
0395             break;
0396         }
0397         default:
0398             break;
0399         }
0400     }
0401 
0402     for (int i = 0; i < tractor.count() && ok; i++) {
0403         qDebug() << "::: PROCESSING TK " << i;
0404         std::unique_ptr<Mlt::Producer> track(tractor.track(i));
0405         if (track->property_exists("kdenlive:playlistid")) {
0406             playlist_name = track->get("kdenlive:playlistid");
0407         } else {
0408             playlist_name = track->get("id");
0409         }
0410         if (reserved_names.contains(playlist_name)) {
0411             if (playlist_name == QLatin1String("timeline_preview")) {
0412                 Mlt::Playlist local_playlist(*track);
0413                 pCore->loadTimelinePreview(timeline->uuid(), chunks, dirty, enablePreview, local_playlist);
0414             }
0415             continue;
0416         }
0417         switch (track->type()) {
0418         case mlt_service_producer_type:
0419             // TODO check that it is the black track, and otherwise log an error
0420             break;
0421         case mlt_service_tractor_type: {
0422             // that is a double track
0423             int tid;
0424             bool audioTrack = track->get_int("kdenlive:audio_track") == 1;
0425             if (!audioTrack) {
0426                 videoTracksIndexes << i;
0427                 vTracks++;
0428             } else {
0429                 aTracks++;
0430             }
0431             ok = timeline->requestTrackInsertion(-1, tid, QString(), audioTrack, undo, redo, false);
0432             if (track->get_int("kdenlive:locked_track") > 0) {
0433                 lockedTracksIndexes << tid;
0434             }
0435             Mlt::Tractor local_tractor(*track);
0436             const QString trackTag = audioTrack ? QStringLiteral("A%1").arg(aTracksCount - aTracks) : QStringLiteral("V%1").arg(vTracks);
0437             ok = ok && constructTrackFromMelt(timeline, tid, true, trackTag, local_tractor, undo, redo, audioTrack, originalDecimalPoint);
0438             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), track->get("kdenlive:thumbs_format"));
0439             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), track->get("kdenlive:audio_rec"));
0440             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:timeline_active"), track->get("kdenlive:timeline_active"));
0441             break;
0442         }
0443         case mlt_service_playlist_type: {
0444             // that is a single track
0445             int tid;
0446             Mlt::Playlist local_playlist(*track);
0447             const QString trackName = local_playlist.get("kdenlive:track_name");
0448             bool audioTrack = local_playlist.get_int("kdenlive:audio_track") == 1;
0449             if (!audioTrack) {
0450                 videoTracksIndexes << i;
0451                 vTracks++;
0452             } else {
0453                 aTracks++;
0454             }
0455             ok = timeline->requestTrackInsertion(-1, tid, trackName, audioTrack, undo, redo, false);
0456             int muteState = track->get_int("hide");
0457             if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) {
0458                 timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState));
0459             }
0460             const QString trackTag = audioTrack ? QStringLiteral("A%1").arg(aTracksCount - aTracks) : QStringLiteral("V%1").arg(vTracks);
0461             ok = ok && constructTrackFromMelt(timeline, tid, true, trackTag, local_playlist, undo, redo, audioTrack, originalDecimalPoint, 0,
0462                                               QList<Mlt::Transition *>());
0463             if (local_playlist.get_int("kdenlive:locked_track") > 0) {
0464                 lockedTracksIndexes << tid;
0465             }
0466             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:thumbs_format"), local_playlist.get("kdenlive:thumbs_format"));
0467             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), track->get("kdenlive:audio_rec"));
0468             timeline->setTrackProperty(tid, QStringLiteral("kdenlive:timeline_active"), track->get("kdenlive:timeline_active"));
0469             break;
0470         }
0471         default:
0472             qWarning() << "Unexpected track type" << track->type();
0473         }
0474     }
0475     timeline->_resetView();
0476 
0477     // Loading compositions
0478     QScopedPointer<Mlt::Service> service(tractor.producer());
0479     QList<Mlt::Transition *> compositions;
0480     while ((service != nullptr) && service->is_valid()) {
0481         if (service->type() == mlt_service_transition_type) {
0482             Mlt::Transition t(mlt_transition(service->get_service()));
0483             if (t.get_b_track() >= timeline->tractor()->count()) {
0484                 // Composition outside of available track, maybe because of a preview track
0485                 service.reset(service->producer());
0486                 continue;
0487             }
0488             QString id(t.get("kdenlive_id"));
0489             if (t.property_exists("internal_added") == false && t.property_exists("kdenlive:mixcut") == false) {
0490                 compositions << new Mlt::Transition(t);
0491                 if (id.isEmpty()) {
0492                     qWarning() << "transition without id" << t.get("id") << t.get("mlt_service") << "on track" << t.get_b_track();
0493                     t.set("kdenlive_id", t.get("mlt_service"));
0494                 }
0495             }
0496         }
0497         service.reset(service->producer());
0498     }
0499     // Sort compositions and insert
0500     bool compositionOk = true;
0501     while (!compositions.isEmpty()) {
0502         QScopedPointer<Mlt::Transition> t(compositions.takeFirst());
0503         QString id(t->get("kdenlive_id"));
0504         int compoId;
0505         int aTrack = t->get_a_track();
0506         if (!timeline->isTrack(timeline->getTrackIndexFromPosition(t->get_b_track() - 1))) {
0507             QString tcInfo = QString("<a href=\"%1!%2\">%3</a>")
0508                                  .arg(timeline->uuid().toString(), QString::number(t->get_in()), pCore->timecode().getTimecodeFromFrames(t->get_in()));
0509             m_notesLog << i18n("%1 Composition (%2) with invalid track reference found and removed.", tcInfo, t->get("id"));
0510             m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3, compositing with track %4.", t->get("id"), t->get_b_track(),
0511                                    pCore->timecode().getTimecodeFromFrames(t->get_in()), t->get_a_track());
0512             continue;
0513         }
0514         if (aTrack > tractor.count()) {
0515             int tid = timeline->getTrackIndexFromPosition(t->get_b_track() - 1);
0516             const QString tcInfo = QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0517                                        .arg(timeline->uuid().toString(), QString::number(t->get_in()), QString::number(timeline->getTrackPosition(tid) + 1),
0518                                             timeline->getTrackTagById(tid), pCore->timecode().getTimecodeFromFrames(t->get_in()));
0519             m_notesLog << i18n("%1 Composition (%2) with invalid track reference found and removed.", tcInfo, t->get("id"));
0520             m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3, compositing with track %4.", t->get("id"), t->get_b_track(),
0521                                    pCore->timecode().getTimecodeFromFrames(t->get_in()), t->get_a_track());
0522             continue;
0523         }
0524         if (t->get_int("force_track") == 0) {
0525             // This is an automatic composition, check that we composite with lower track or warn
0526             int pos = videoTracksIndexes.indexOf(t->get_b_track());
0527             if (pos > 0 && videoTracksIndexes.at(pos - 1) != aTrack) {
0528                 t->set("force_track", 1);
0529                 int tid = timeline->getTrackIndexFromPosition(t->get_b_track() - 1);
0530                 const QString tcInfo = QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0531                                            .arg(timeline->uuid().toString(), QString::number(t->get_in()), QString::number(timeline->getTrackPosition(tid) + 1),
0532                                                 timeline->getTrackTagById(tid), pCore->timecode().getTimecodeFromFrames(t->get_in()));
0533                 m_notesLog << i18n("%1 Composition was not applied on expected track, manually enforce the track.", tcInfo);
0534                 m_errorMessage << i18n("Incorrect composition %1 found on track %2 at %3, compositing with track %4 was set to forced track.", t->get("id"),
0535                                        t->get_b_track(), pCore->timecode().getTimecodeFromFrames(t->get_in()), t->get_a_track());
0536             }
0537         }
0538         auto transProps = std::make_unique<Mlt::Properties>(t->get_properties());
0539         compositionOk = timeline->requestCompositionInsertion(id, timeline->getTrackIndexFromPosition(t->get_b_track() - 1), t->get_a_track(), t->get_in(),
0540                                                               t->get_length(), std::move(transProps), compoId, undo, redo, false, originalDecimalPoint);
0541         if (!compositionOk) {
0542             // timeline->requestItemDeletion(compoId, false);
0543             int tid = timeline->getTrackIndexFromPosition(t->get_b_track() - 1);
0544             const QString tcInfo = QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0545                                        .arg(timeline->uuid().toString(), QString::number(t->get_in()), QString::number(timeline->getTrackPosition(tid) + 1),
0546                                             timeline->getTrackTagById(tid), pCore->timecode().getTimecodeFromFrames(t->get_in()));
0547             m_notesLog << i18n("%1 Invalid composition found and removed.", tcInfo);
0548             m_errorMessage << i18n("Invalid composition %1 found on track %2 at %3.", t->get("id"), t->get_b_track(),
0549                                    pCore->timecode().getTimecodeFromFrames(t->get_in()));
0550             continue;
0551         }
0552     }
0553     qDeleteAll(compositions);
0554 
0555     // build internal track compositing
0556     timeline->buildTrackCompositing();
0557 
0558     // load locked state as last step
0559     for (int tid : qAsConst(lockedTracksIndexes)) {
0560         timeline->lockTrack(tid, true);
0561     }
0562 
0563     if (!ok) {
0564         // Loading tracks failed, abort loading
0565         qDebug() << "IIIIIIIIIIIIIIIIII\nFAILED LOADING TIMELINE BBBBBBBBBBBBBBBBBBBB";
0566         return false;
0567     }
0568     if (!qEnvironmentVariableIsSet("MLT_TESTS")) {
0569         if (!m_notesLog.isEmpty()) {
0570             if (projectErrors) {
0571                 *projectErrors = true;
0572             }
0573         }
0574     }
0575     timeline->isLoading = false;
0576     return true;
0577 }
0578 
0579 void checkProjectWarnings()
0580 {
0581     if (!qEnvironmentVariableIsSet("MLT_TESTS")) {
0582         if (!m_notesLog.isEmpty()) {
0583             m_notesLog.prepend(i18n("Errors found when opening project file (%1)", QDateTime::currentDateTime().toString()));
0584             pCore->projectManager()->slotAddTextNote(m_notesLog.join("<br/>"));
0585             KMessageBox::detailedError(
0586                 qApp->activeWindow(),
0587                 i18n("Some errors were detected in the project file.\nThe project was modified to fix the conflicts. Changes made to the "
0588                      "project have been listed in the Project Notes tab,\nplease review them to ensure your project integrity."),
0589                 m_errorMessage.join("\n"), i18n("Problems found in your project file"));
0590         } else if (!m_errorMessage.isEmpty()) {
0591             KMessageBox::error(qApp->activeWindow(), m_errorMessage.join("\n"), i18n("Problems found in your project file"));
0592         }
0593     }
0594 }
0595 
0596 bool constructTrackFromMelt(const std::shared_ptr<TimelineItemModel> &timeline, int tid, bool useMappedIds, const QString trackTag, Mlt::Tractor &track,
0597                             Fun &undo, Fun &redo, bool audioTrack, const QString &originalDecimalPoint)
0598 {
0599     if (track.count() != 2) {
0600         // we expect a tractor with two tracks (a "fake" track)
0601         qWarning() << "wrong number of subtracks";
0602         return false;
0603     }
0604     // Check same track transitions
0605     QScopedPointer<Mlt::Service> service(track.field());
0606     QList<Mlt::Transition *> compositions;
0607     while ((service != nullptr) && service->is_valid()) {
0608         if (service->type() == mlt_service_transition_type) {
0609             Mlt::Transition t(mlt_transition(service->get_service()));
0610             QString id(t.get("kdenlive_id"));
0611             compositions << new Mlt::Transition(t);
0612             if (id.isEmpty()) {
0613                 qWarning() << "transition without id" << t.get("id") << t.get("mlt_service");
0614                 t.set("kdenlive_id", t.get("mlt_service"));
0615             }
0616         }
0617         service.reset(service->producer());
0618     }
0619     for (int i = 0; i < track.count(); i++) {
0620         std::unique_ptr<Mlt::Producer> sub_track(track.track(i));
0621         if (sub_track->type() != mlt_service_playlist_type) {
0622             qWarning() << "subtrack must be playlist";
0623             return false;
0624         }
0625         Mlt::Playlist playlist(*sub_track);
0626         constructTrackFromMelt(timeline, tid, useMappedIds, trackTag, playlist, undo, redo, audioTrack, originalDecimalPoint, i, compositions);
0627         if (i == 0) {
0628             // Pass track properties
0629             int height = track.get_int("kdenlive:trackheight");
0630             timeline->setTrackProperty(tid, "kdenlive:trackheight", height == 0 ? "100" : QString::number(height));
0631             timeline->setTrackProperty(tid, "kdenlive:collapsed", QString::number(track.get_int("kdenlive:collapsed")));
0632             QString trackName = track.get("kdenlive:track_name");
0633             if (!trackName.isEmpty()) {
0634                 timeline->setTrackProperty(tid, QStringLiteral("kdenlive:track_name"), trackName.toUtf8().constData());
0635             }
0636             if (audioTrack) {
0637                 // This is an audio track
0638                 timeline->setTrackProperty(tid, QStringLiteral("kdenlive:audio_track"), QStringLiteral("1"));
0639                 timeline->setTrackProperty(tid, QStringLiteral("hide"), QStringLiteral("1"));
0640             } else {
0641                 // video track, hide audio
0642                 timeline->setTrackProperty(tid, QStringLiteral("hide"), QStringLiteral("2"));
0643             }
0644             int muteState = playlist.get_int("hide");
0645             if (muteState > 0 && (!audioTrack || (audioTrack && muteState != 1))) {
0646                 timeline->setTrackProperty(tid, QStringLiteral("hide"), QString::number(muteState));
0647             }
0648         }
0649     }
0650     // Load same track mixes
0651     for (auto compo : qAsConst(compositions)) {
0652         if (!timeline->plantMix(tid, compo)) {
0653             // There is an error with a mix, we have overlapping clips
0654             int in = compo->get_in();
0655             int out = compo->get_out() - 1;
0656             bool reverse = compo->get_int("reverse") == 1;
0657             int cid1 = timeline->getClipByPosition(tid, in, reverse ? 1 : 0);
0658             int cid2 = timeline->getClipByPosition(tid, out, reverse ? 0 : 1);
0659             if (cid1 > 0 && cid2 > 0) {
0660                 if (timeline->getClipSubPlaylistIndex(cid1) == 1) {
0661                     // Delete clip
0662                     const QString tcInfo = QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0663                                                .arg(timeline->uuid().toString(), QString::number(in), QString::number(timeline->getTrackPosition(tid) + 1),
0664                                                     timeline->getTrackTagById(tid), pCore->timecode().getTimecodeFromFrames(timeline->getClipPosition(cid1)));
0665                     m_notesLog << i18n("Incorrect Mix, clip %1 was removed at %2.", timeline->getClipName(cid1), tcInfo);
0666                     m_errorMessage << i18n("Incorrect mix found on track %1 at %2, clip %3 removed.", timeline->getTrackTagById(tid),
0667                                            pCore->timecode().getTimecodeFromFrames(timeline->getClipPosition(cid1)), timeline->getClipName(cid1));
0668                     timeline->requestItemDeletion(cid1, false);
0669 
0670                 } else if (timeline->getClipSubPlaylistIndex(cid2) == 1) {
0671                     // Delete clip
0672                     const QString tcInfo = QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0673                                                .arg(timeline->uuid().toString(), QString::number(in), QString::number(timeline->getTrackPosition(tid) + 1),
0674                                                     timeline->getTrackTagById(tid), pCore->timecode().getTimecodeFromFrames(timeline->getClipPosition(cid2)));
0675                     m_notesLog << i18n("Incorrect Mix, clip %1 was removed at %2.", timeline->getClipName(cid2), tcInfo);
0676                     m_errorMessage << i18n("Incorrect mix found at track %1 at %2, clip %3 removed.", timeline->getTrackTagById(tid),
0677                                            pCore->timecode().getTimecodeFromFrames(timeline->getClipPosition(cid2)), timeline->getClipName(cid2));
0678                     timeline->requestItemDeletion(cid2, false);
0679                 }
0680             }
0681         }
0682     }
0683     std::shared_ptr<Mlt::Service> serv = std::make_shared<Mlt::Service>(track.get_service());
0684     timeline->importTrackEffects(tid, serv);
0685     return true;
0686 }
0687 
0688 namespace {
0689 
0690 // This function tries to recover the state of the producer (audio or video or both)
0691 PlaylistState::ClipState inferState(const std::shared_ptr<Mlt::Producer> &prod, bool audioTrack)
0692 {
0693     auto getProperty = [prod](const QString &name) {
0694         if (prod->parent().is_valid()) {
0695             return QString::fromUtf8(prod->parent().get(name.toUtf8().constData()));
0696         }
0697         return QString::fromUtf8(prod->get(name.toUtf8().constData()));
0698     };
0699     auto getIntProperty = [prod](const QString &name) {
0700         if (prod->parent().is_valid()) {
0701             return prod->parent().get_int(name.toUtf8().constData());
0702         }
0703         return prod->get_int(name.toUtf8().constData());
0704     };
0705     QString service = getProperty("mlt_service");
0706     std::pair<bool, bool> VidAud{true, true};
0707     VidAud.first = getIntProperty("set.test_image") == 0;
0708     VidAud.second = getIntProperty("set.test_audio") == 0;
0709     if (audioTrack || ((service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("video_index")) == -1))) {
0710         VidAud.first = false;
0711     }
0712     if (!audioTrack || ((service.contains(QStringLiteral("avformat")) && getIntProperty(QStringLiteral("audio_index")) == -1))) {
0713         VidAud.second = false;
0714     }
0715     return stateFromBool(VidAud);
0716 }
0717 } // namespace
0718 
0719 bool constructTrackFromMelt(const std::shared_ptr<TimelineItemModel> &timeline, int tid, bool useMappedIds, const QString trackTag, Mlt::Playlist &track,
0720                             Fun &undo, Fun &redo, bool audioTrack, const QString &originalDecimalPoint, int playlist,
0721                             const QList<Mlt::Transition *> &compositions)
0722 {
0723     int max = track.count();
0724     const QString sequenceBinId = pCore->projectItemModel()->getSequenceId(timeline->uuid());
0725     for (int i = 0; i < max; i++) {
0726         if (track.is_blank(i)) {
0727             continue;
0728         }
0729         std::shared_ptr<Mlt::Producer> clip(track.get_clip(i));
0730         int position = track.clip_start(i);
0731         switch (clip->type()) {
0732         case mlt_service_unknown_type:
0733         case mlt_service_chain_type:
0734         case mlt_service_producer_type: {
0735             QString binId;
0736             if (clip->parent().get_int("_kdenlive_processed") == 1) {
0737                 // This is a bin clip, already processed no need to change id
0738                 binId = QString(clip->parent().get("kdenlive:id"));
0739             } else {
0740                 QString clipId = clip->parent().get("kdenlive:id");
0741                 if (clipId.startsWith(QStringLiteral("slowmotion"))) {
0742                     clipId = clipId.section(QLatin1Char(':'), 1, 1);
0743                 }
0744                 if (clipId.isEmpty()) {
0745                     clipId = clip->get("kdenlive:id");
0746                 }
0747                 const QString service = clip->parent().get("mlt_service");
0748                 QString resource = service == QLatin1String("timewarp") ? clip->parent().get("warp_resource") : clip->parent().get("resource");
0749                 if (!useMappedIds || binIdCorresp.size() == 0 || (clip->parent().get_int("kdenlive:producer_type") == ClipType::Timeline)) {
0750                     // Currently "sequence" clips inserted in timeline are cuts of the bin clip, so it's kdenlive id is changed in loadBinPlaylist
0751                     if (clip->parent().get_int("kdenlive:producer_type") == ClipType::Timeline && binIdCorresp.count(clipId) > 0 &&
0752                         !clip->parent().property_exists("_kdenlive_processed")) {
0753                         binId = binIdCorresp.at(clipId);
0754                     } else {
0755                         binId = clipId;
0756                     }
0757                     // Ensure we don't try to embed a sequence into itself
0758                     if (binId == sequenceBinId) {
0759                         // Trying to embed a sequence into itself, drop
0760                         const QString tcInfo =
0761                             QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0762                                 .arg(timeline->uuid().toString(), QString::number(position), QString::number(timeline->getTrackPosition(tid) + 1), trackTag,
0763                                      pCore->timecode().getTimecodeFromFrames(position));
0764                         m_notesLog << i18n("%1 Timeline clip (%2) without bin reference found and removed.", tcInfo, clip->parent().get("id"));
0765                         m_errorMessage << i18n("Project corrupted. Clip %1 (%2) not found in project bin.", clip->parent().get("id"), clipId);
0766                         continue;
0767                     }
0768                     // Check that the sequence clip exists
0769                     auto binClip = pCore->projectItemModel()->getClipByBinID(binId);
0770                     if (binClip == nullptr || binClip->getProducerIntProperty("kdenlive:producer_type") != clip->parent().get_int("kdenlive:producer_type")) {
0771                         // Sequence clip not found, error out
0772                         const QString tcInfo =
0773                             QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0774                                 .arg(timeline->uuid().toString(), QString::number(position), QString::number(timeline->getTrackPosition(tid) + 1), trackTag,
0775                                      pCore->timecode().getTimecodeFromFrames(position));
0776                         m_notesLog << i18n("%1 Timeline clip (%2) without bin reference found and removed.", tcInfo, clip->parent().get("id"));
0777                         m_errorMessage << i18n("Project corrupted. Clip %1 (%2) not found in project bin.", clip->parent().get("id"), clipId);
0778                         continue;
0779                     }
0780                 } else if (binIdCorresp.count(clipId) == 0) {
0781                     if (clip->property_exists("kdenlive:remove")) {
0782                         // Clip was marked for deletion
0783                         continue;
0784                     }
0785                     // Project was somehow corrupted
0786                     qWarning() << "can't find clip with id: " << clipId << "in bin playlist";
0787                     QStringList fixedId = pCore->projectItemModel()->getClipByUrl(QFileInfo(resource));
0788                     const QString tcInfo =
0789                         QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0790                             .arg(timeline->uuid().toString(), QString::number(position), QString::number(timeline->getTrackPosition(tid) + 1), trackTag,
0791                                  pCore->timecode().getTimecodeFromFrames(position));
0792                     if (!fixedId.isEmpty()) {
0793                         binId = fixedId.first();
0794                         m_notesLog << i18n("%1 Timeline clip (%2) with incorrect bin reference found and recovered.", tcInfo, clip->parent().get("id"));
0795                         m_errorMessage << i18n("Invalid clip %1 (%2) not found in project bin, recovered.", clip->parent().get("id"), clipId);
0796                     } else {
0797                         m_notesLog << i18n("%1 Timeline clip (%2) without bin reference found and removed.", tcInfo, clip->parent().get("id"));
0798                         m_errorMessage << i18n("Project corrupted. Clip %1 (%2) not found in project bin.", clip->parent().get("id"), clipId);
0799                         // Do not try to insert clip
0800                         continue;
0801                     }
0802                 } else {
0803                     binId = binIdCorresp.at(clipId);
0804                 }
0805                 Q_ASSERT(!clipId.isEmpty() && !binId.isEmpty());
0806                 clip->parent().set("kdenlive:id", binId.toUtf8().constData());
0807                 clip->parent().set("_kdenlive_processed", 1);
0808             }
0809             bool ok = false;
0810             int cid = -1;
0811             if (pCore->projectItemModel()->getClipByBinID(binId) == nullptr) {
0812                 // Trying to recover clip by its resource
0813                 const QString service = clip->parent().get("mlt_service");
0814                 const QString resource = service == QLatin1String("timewarp") ? clip->parent().get("warp_resource") : clip->parent().get("resource");
0815                 const QStringList possibleIds = pCore->projectItemModel()->getClipByUrl(QFileInfo(resource));
0816                 qWarning() << "Incorred clip id, trying to recover " << binId << "/" << clip->get("id") << " = " << resource;
0817                 if (!possibleIds.isEmpty()) {
0818                     binId = possibleIds.first();
0819                     clip->parent().set("kdenlive:id", binId.toUtf8().constData());
0820                     qDebug() << "=== FOUND POSSIBLE MATCHES: " << possibleIds;
0821                 }
0822             }
0823             if (pCore->projectItemModel()->getClipByBinID(binId)) {
0824                 PlaylistState::ClipState st = inferState(clip, audioTrack);
0825                 bool enforceTopPlaylist = false;
0826                 if (playlist > 0) {
0827                     // Clips on playlist > 0 must have a mix or something is wrong
0828                     bool hasStartMix = !timeline->trackIsBlankAt(tid, position, 0);
0829                     int duration = clip->get_playtime() - 1;
0830                     bool hasEndMix = !timeline->trackIsBlankAt(tid, position + duration, 0);
0831                     bool startMixToFind = hasStartMix;
0832                     bool endMixToFind = hasEndMix;
0833                     if (startMixToFind || endMixToFind) {
0834                         for (auto &compo : compositions) {
0835                             int in = compo->get_in();
0836                             int out = compo->get_out();
0837                             if (startMixToFind && out > position && in <= position) {
0838                                 startMixToFind = false;
0839                             }
0840                             if (endMixToFind && in < position + duration && out >= position + duration) {
0841                                 endMixToFind = false;
0842                             }
0843                             if (!startMixToFind && !endMixToFind) {
0844                                 break;
0845                             }
0846                         }
0847                         if (startMixToFind || endMixToFind) {
0848                             // A mix for this clip is missing
0849                             const QString tcInfo =
0850                                 QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0851                                     .arg(timeline->uuid().toString(), QString::number(position), QString::number(timeline->getTrackPosition(tid) + 1), trackTag,
0852                                          pCore->timecode().getTimecodeFromFrames(position));
0853                             // Try to resize clip and put it on playlist 0
0854                             if (hasEndMix && endMixToFind) {
0855                                 // Find last empty frame on playlist 0
0856                                 int lastAvailableFrame = timeline->getClipStartAt(tid, position + duration, 0);
0857                                 if (lastAvailableFrame > position) {
0858                                     // Resize clip to fit.
0859                                     int updatedDuration = lastAvailableFrame - position - 1;
0860                                     int currentIn = clip->get_in();
0861                                     clip->set_in_and_out(currentIn, currentIn + updatedDuration);
0862                                     hasEndMix = false;
0863                                     if (!startMixToFind) {
0864                                         // Move to top playlist
0865                                         cid = ClipModel::construct(timeline, binId, clip, st, tid, originalDecimalPoint, hasStartMix ? playlist : 0);
0866                                         timeline->requestClipMove(cid, tid, position, true, true, false, true, undo, redo);
0867                                         m_notesLog << i18n("%1 Clip (%2) with missing mix found and resized", tcInfo, clip->parent().get("id"));
0868                                         m_errorMessage << i18n("Clip without mix %1 found and resized on track %2 at %3.", clip->parent().get("id"), trackTag,
0869                                                                pCore->timecode().getTimecodeFromFrames(position));
0870                                         continue;
0871                                     }
0872                                 } else {
0873                                     m_errorMessage << i18n("Invalid clip without mix %1 found and removed on track %2 at %3.", clip->parent().get("id"),
0874                                                            trackTag, pCore->timecode().getTimecodeFromFrames(position));
0875                                     m_notesLog << i18n("%1 Clip (%2) with missing mix found and removed", tcInfo, clip->parent().get("id"));
0876                                     continue;
0877                                 }
0878                             }
0879                             if (hasStartMix && startMixToFind) {
0880                                 int firstAvailableFrame = timeline->getClipEndAt(tid, position, 0);
0881                                 if (firstAvailableFrame < position + duration) {
0882                                     int delta = firstAvailableFrame - position;
0883                                     int currentIn = clip->get_in();
0884                                     currentIn += delta;
0885                                     position += delta;
0886                                     int currentOut = clip->get_out();
0887                                     clip->set_in_and_out(currentIn, currentOut);
0888                                     // Move to top playlist
0889                                     cid = ClipModel::construct(timeline, binId, clip, st, tid, originalDecimalPoint, hasEndMix ? playlist : 0);
0890                                     ok = timeline->requestClipMove(cid, tid, position, true, true, false, true, undo, redo);
0891                                     if (!ok && cid > -1) {
0892                                         timeline->requestItemDeletion(cid, false);
0893                                         m_errorMessage << i18n("Invalid clip %1 found on track %2 at %3.", clip->parent().get("id"), track.get("id"),
0894                                                                pCore->timecode().getTimecodeFromFrames(position));
0895                                         m_notesLog << i18n("%1 Invalid clip (%2) found and removed", tcInfo, clip->parent().get("id"));
0896                                         continue;
0897                                     }
0898                                     m_errorMessage << i18n("Clip without mix %1 found and resized on track %2 at %3.", clip->parent().get("id"), trackTag,
0899                                                            pCore->timecode().getTimecodeFromFrames(position));
0900                                     m_notesLog << i18n("%1 Clip (%2) with missing mix found and resized", tcInfo, clip->parent().get("id"));
0901                                     continue;
0902                                 }
0903                             }
0904                             m_errorMessage << i18n("Invalid clip without mix %1 found and removed on track %2 at %3.", clip->parent().get("id"), trackTag,
0905                                                    pCore->timecode().getTimecodeFromFrames(position));
0906                             m_notesLog << i18n("%1 Clip (%2) with missing mix found and removed", tcInfo, clip->parent().get("id"));
0907                             continue;
0908                         }
0909                     } else {
0910                         // Check if playlist 0 is available
0911                         enforceTopPlaylist = timeline->trackIsAvailable(tid, position, duration, 0);
0912                         const QString tcInfo =
0913                             QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0914                                 .arg(timeline->uuid().toString(), QString::number(position), QString::number(timeline->getTrackPosition(tid) + 1), trackTag,
0915                                      pCore->timecode().getTimecodeFromFrames(position));
0916                         if (enforceTopPlaylist) {
0917                             m_errorMessage << i18n("Clip %1 on incorrect subtrack found and fixed on track %2 at %3.", clip->parent().get("id"), trackTag,
0918                                                    pCore->timecode().getTimecodeFromFrames(position));
0919                             m_notesLog << i18n("%1 Clip (%2) on incorrect subtrack found and fixed / %3", tcInfo, clip->parent().get("id"),
0920                                                QString::number(tid));
0921                         } else {
0922                             m_errorMessage << i18n("Clip %1 on incorrect subtrack found on track %2 at %3.", clip->parent().get("id"), trackTag,
0923                                                    pCore->timecode().getTimecodeFromFrames(position));
0924                             m_notesLog << i18n("%1 Clip (%2) with incorrect subplaylist found", tcInfo, clip->parent().get("id"));
0925                         }
0926                     }
0927                 }
0928                 cid = ClipModel::construct(timeline, binId, clip, st, tid, originalDecimalPoint, enforceTopPlaylist ? 0 : playlist);
0929                 ok = timeline->requestClipMove(cid, tid, position, true, true, false, true, undo, redo);
0930             } else {
0931                 qWarning() << "Really can't find bin clip" << binId << clip->get("id");
0932             }
0933             if (!ok && cid > -1) {
0934                 timeline->requestItemDeletion(cid, false);
0935                 m_errorMessage << i18n("Invalid clip %1 found on track %2 at %3.", clip->parent().get("id"), track.get("id"),
0936                                        pCore->timecode().getTimecodeFromFrames(position));
0937                 const QString tcInfo = QString("<a href=\"%1!%2?%3\">%4 %5</a>")
0938                                            .arg(timeline->uuid().toString(), QString::number(position), QString::number(timeline->getTrackPosition(tid) + 1),
0939                                                 trackTag, pCore->timecode().getTimecodeFromFrames(position));
0940                 m_notesLog << i18n("%1 Invalid clip (%2) found and removed", tcInfo, clip->parent().get("id"));
0941                 continue;
0942             }
0943             break;
0944         }
0945         case mlt_service_tractor_type: {
0946             // TODO This is a nested timeline
0947             qWarning() << "nested timelines not yet implemented";
0948             Q_ASSERT(false);
0949             break;
0950         }
0951         default:
0952             qWarning() << "unexpected object found in playlist";
0953             return false;
0954             break;
0955         }
0956     }
0957     std::shared_ptr<Mlt::Service> serv = std::make_shared<Mlt::Service>(track.get_service());
0958     timeline->importTrackEffects(tid, serv);
0959     return true;
0960 }