File indexing completed on 2024-04-21 04:49:01

0001 /*
0002    SPDX-FileCopyrightText: 2015 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
0003    SPDX-FileCopyrightText: 2019 (c) Alexander Stippich <a.stippich@gmx.net>
0004    SPDX-FileCopyrightText: 2020 (c) Carson Black <uhhadd@gmail.com>
0005 
0006    SPDX-License-Identifier: LGPL-3.0-or-later
0007  */
0008 
0009 #include "mediaplaylistproxymodel.h"
0010 #include "elisautils.h"
0011 #include "mediaplaylist.h"
0012 #include "playListLogging.h"
0013 #include "elisa_settings.h"
0014 #include "config-upnp-qt.h"
0015 #include <QItemSelection>
0016 #include <QList>
0017 #include <QRandomGenerator>
0018 #include <QFile>
0019 #include <QFileInfo>
0020 #include <QDir>
0021 #include <QMimeDatabase>
0022 
0023 #if KFKIO_FOUND
0024 #include <KIO/OpenUrlJob>
0025 #endif
0026 
0027 #include <algorithm>
0028 
0029 QList<QUrl> M3uPlaylistParser::fromPlaylist(const QUrl &fileName, const QByteArray &fileContent) {
0030     Q_UNUSED(fileName);
0031     QList<QUrl> result;
0032 
0033     for (const QByteArray &l : fileContent.split('\n')) {
0034         const QString &line = QString::fromUtf8(l);
0035         if (!line.isEmpty() && !line.startsWith(QStringLiteral("#"))) {
0036             const QUrl &url = line.contains(QStringLiteral("://")) ? QUrl(line) : QUrl::fromLocalFile(line);
0037             result.append(url);
0038         }
0039     }
0040 
0041     return result;
0042 }
0043 
0044 QString M3uPlaylistParser::toPlaylist(const QUrl &fileName, const QList<QString> &listOfUrls) {
0045     Q_UNUSED(fileName);
0046     QString result;
0047 
0048     for (const QString &line : listOfUrls) {
0049         if (!line.isEmpty()) {
0050             result += line + QStringLiteral("\n");
0051         }
0052     }
0053 
0054     return result;
0055 }
0056 
0057 QList<QUrl> PlsPlaylistParser::fromPlaylist(const QUrl &fileName, const QByteArray &fileContent) {
0058     Q_UNUSED(fileName);
0059     QList<QUrl> result;
0060     for (const QByteArray &l : fileContent.split('\n')) {
0061         const QString &line = QString::fromUtf8(l);
0062         if (!line.isEmpty() && line.startsWith(QStringLiteral("File")) && line.contains(QStringLiteral("="))) {
0063             int indexOfEquals = line.indexOf(QStringLiteral("="));
0064             QString urlAsString = line.mid(indexOfEquals + QStringLiteral("=").length());
0065             const QUrl &url = urlAsString.contains(QStringLiteral("://")) ? QUrl(urlAsString) : QUrl::fromLocalFile(urlAsString);
0066             result.append(url);
0067         }
0068     }
0069 
0070     return result;
0071 }
0072 
0073 QString PlsPlaylistParser::toPlaylist(const QUrl &fileName, const QList<QString> &listOfUrls) {
0074     Q_UNUSED(fileName);
0075     QString result = QStringLiteral(R"--([playlist]
0076 
0077 Version=2
0078 NumberOfEntries=%1
0079 )--").arg(listOfUrls.count());
0080 
0081     // Sample:
0082 /*[playlist]
0083 
0084 Version=2
0085 NumberOfEntries=2
0086 
0087 File1=http://example.example:8068
0088 
0089 File2=example2.mp3
0090 */
0091 
0092     int counter=0;
0093     for (const QString &line : listOfUrls) {
0094         if (!line.isEmpty()) {
0095             QString oneLine = QStringLiteral("\nFile%1=%2\n").arg(counter + 1).arg(line);
0096             result += oneLine;
0097             counter++;
0098         }
0099     }
0100 
0101     return result;
0102 }
0103 
0104 QList<QUrl> PlaylistParser::fromPlaylist(const QUrl &fileName, const QByteArray &fileContent) {
0105     QList<QUrl> result;
0106 
0107     if (fileName.isValid() && !fileName.isEmpty()) {
0108         auto mimeType = QMimeDatabase().mimeTypeForUrl(fileName);
0109 
0110         if (mimeType.inherits(QStringLiteral("audio/x-scpls"))) {
0111             PlsPlaylistParser plsPlaylistParser;
0112             result = plsPlaylistParser.fromPlaylist(fileName, fileContent);
0113         } else if (mimeType.name().contains(QStringLiteral("mpegurl"))) {
0114             M3uPlaylistParser m3uPlaylistParser;
0115             result = m3uPlaylistParser.fromPlaylist(fileName, fileContent);
0116         }
0117     }
0118 
0119     return result;
0120 }
0121 
0122 QString PlaylistParser::toPlaylist(const QUrl &fileName, const QList<QString> &listOfUrls) {
0123     QString result;
0124 
0125     if (fileName.isValid() && !fileName.isEmpty()) {
0126         auto mimeType = QMimeDatabase().mimeTypeForUrl(fileName);
0127 
0128         if (mimeType.inherits(QStringLiteral("audio/x-scpls"))) {
0129             PlsPlaylistParser plsPlaylistParser;
0130             result = plsPlaylistParser.toPlaylist(fileName, listOfUrls);
0131         } else if (mimeType.name().contains(QStringLiteral("mpegurl"))) {
0132             M3uPlaylistParser m3uPlaylistParser;
0133             result = m3uPlaylistParser.toPlaylist(fileName, listOfUrls);
0134         }
0135     }
0136 
0137     return result;
0138 }
0139 
0140 class MediaPlayListProxyModelPrivate
0141 {
0142 public:
0143 
0144     MediaPlayList* mPlayListModel;
0145 
0146     QPersistentModelIndex mPreviousTrack;
0147 
0148     QPersistentModelIndex mCurrentTrack;
0149 
0150     bool mCurrentTrackWasValid = false;
0151 
0152     QPersistentModelIndex mNextTrack;
0153 
0154     QList<int> mRandomMapping;
0155 
0156     QVariantMap mPersistentSettingsForUndo;
0157 
0158     QRandomGenerator mRandomGenerator;
0159 
0160     QMimeDatabase mMimeDb;
0161 
0162     ElisaUtils::PlayListEnqueueTriggerPlay mTriggerPlay = ElisaUtils::DoNotTriggerPlay;
0163 
0164     int mCurrentPlayListPosition = -1;
0165 
0166     MediaPlayListProxyModel::Repeat mRepeatMode = MediaPlayListProxyModel::Repeat::None;
0167 
0168     bool mShufflePlayList = false;
0169 
0170     bool mPartiallyLoaded = false;
0171 
0172     QUrl mLoadedPlayListUrl;
0173 
0174 };
0175 
0176 MediaPlayListProxyModel::MediaPlayListProxyModel(QObject *parent) : QAbstractProxyModel (parent),
0177     d(std::make_unique<MediaPlayListProxyModelPrivate>())
0178 {
0179     d->mRandomGenerator.seed(static_cast<unsigned int>(QTime::currentTime().msec()));
0180 }
0181 
0182 MediaPlayListProxyModel::~MediaPlayListProxyModel()
0183 =default;
0184 
0185 QModelIndex MediaPlayListProxyModel::index(int row, int column, const QModelIndex &parent) const
0186 {
0187     if (row < 0 || column < 0 || row > rowCount() - 1) {
0188         return QModelIndex();
0189     }
0190     return createIndex(row, column);
0191     Q_UNUSED(parent);
0192 }
0193 
0194 QModelIndex MediaPlayListProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
0195 {
0196     if (!sourceIndex.isValid()) {
0197         return QModelIndex();
0198     }
0199     return d->mPlayListModel->index(mapRowFromSource(sourceIndex.row()), sourceIndex.column());
0200 }
0201 
0202 QItemSelection MediaPlayListProxyModel::mapSelectionFromSource(const QItemSelection &sourceSelection) const
0203 {
0204     QItemSelection proxySelection;
0205     for (const QItemSelectionRange &range : sourceSelection) {
0206         QModelIndex proxyTopLeft = mapFromSource(range.topLeft());
0207         QModelIndex proxyBottomRight = mapFromSource(range.bottomRight());
0208         proxySelection.append(QItemSelectionRange(proxyTopLeft, proxyBottomRight));
0209     }
0210     return proxySelection;
0211 }
0212 
0213 QItemSelection MediaPlayListProxyModel::mapSelectionToSource(const QItemSelection &proxySelection) const
0214 {
0215     QItemSelection sourceSelection;
0216     for (const QItemSelectionRange &range : proxySelection) {
0217         QModelIndex sourceTopLeft = mapToSource(range.topLeft());
0218         QModelIndex sourceBottomRight = mapToSource(range.bottomRight());
0219         sourceSelection.append(QItemSelectionRange(sourceTopLeft, sourceBottomRight));
0220     }
0221     return sourceSelection;
0222 }
0223 
0224 QModelIndex MediaPlayListProxyModel::mapToSource(const QModelIndex &proxyIndex) const
0225 {
0226     if (!proxyIndex.isValid()) {
0227         return QModelIndex();
0228     }
0229     return d->mPlayListModel->index(mapRowToSource(proxyIndex.row()), proxyIndex.column());
0230 }
0231 
0232 int MediaPlayListProxyModel::mapRowToSource(const int proxyRow) const
0233 {
0234     if (d->mShufflePlayList) {
0235         return d->mRandomMapping.at(proxyRow);
0236     } else {
0237         return proxyRow;
0238     }
0239 }
0240 
0241 int MediaPlayListProxyModel::mapRowFromSource(const int sourceRow) const
0242 {
0243     if (d->mShufflePlayList) {
0244         return d->mRandomMapping.indexOf(sourceRow);
0245     } else {
0246         return sourceRow;
0247     }
0248 }
0249 
0250 int MediaPlayListProxyModel::rowCount(const QModelIndex &parent) const
0251 {
0252     if (d->mShufflePlayList) {
0253         if (parent.isValid()) {
0254             return 0;
0255         }
0256         return d->mRandomMapping.count();
0257     } else {
0258         return d->mPlayListModel->rowCount(parent);
0259     }
0260 }
0261 
0262 int MediaPlayListProxyModel::columnCount(const QModelIndex &parent) const
0263 {
0264     Q_UNUSED(parent);
0265     return 1;
0266 }
0267 
0268 QModelIndex MediaPlayListProxyModel::parent(const QModelIndex &child) const
0269 {
0270     Q_UNUSED(child);
0271     return QModelIndex();
0272 }
0273 
0274 bool MediaPlayListProxyModel::hasChildren(const QModelIndex &parent) const
0275 {
0276     return (parent.isValid()) ? false : (rowCount() > 0);
0277 }
0278 
0279 void MediaPlayListProxyModel::setPlayListModel(MediaPlayList *playListModel)
0280 {
0281     if (d->mPlayListModel)
0282     {
0283         disconnect(playListModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &MediaPlayListProxyModel::sourceRowsAboutToBeInserted);
0284         disconnect(playListModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MediaPlayListProxyModel::sourceRowsAboutToBeRemoved);
0285         disconnect(playListModel, &QAbstractItemModel::rowsAboutToBeMoved, this, &MediaPlayListProxyModel::sourceRowsAboutToBeMoved);
0286         disconnect(playListModel, &QAbstractItemModel::rowsInserted, this, &MediaPlayListProxyModel::sourceRowsInserted);
0287         disconnect(playListModel, &QAbstractItemModel::rowsRemoved, this, &MediaPlayListProxyModel::sourceRowsRemoved);
0288         disconnect(playListModel, &QAbstractItemModel::rowsMoved, this, &MediaPlayListProxyModel::sourceRowsMoved);
0289         disconnect(playListModel, &QAbstractItemModel::dataChanged, this, &MediaPlayListProxyModel::sourceDataChanged);
0290         disconnect(playListModel, &QAbstractItemModel::headerDataChanged, this, &MediaPlayListProxyModel::sourceHeaderDataChanged);
0291         disconnect(playListModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &MediaPlayListProxyModel::sourceLayoutAboutToBeChanged);
0292         disconnect(playListModel, &QAbstractItemModel::layoutChanged, this, &MediaPlayListProxyModel::sourceLayoutChanged);
0293         disconnect(playListModel, &QAbstractItemModel::modelAboutToBeReset, this, &MediaPlayListProxyModel::sourceModelAboutToBeReset);
0294         disconnect(playListModel, &QAbstractItemModel::modelReset, this, &MediaPlayListProxyModel::sourceModelReset);
0295     }
0296 
0297     d->mPlayListModel = playListModel;
0298     setSourceModel(playListModel);
0299 
0300     if (d->mPlayListModel) {
0301         connect(playListModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &MediaPlayListProxyModel::sourceRowsAboutToBeInserted);
0302         connect(playListModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MediaPlayListProxyModel::sourceRowsAboutToBeRemoved);
0303         connect(playListModel, &QAbstractItemModel::rowsAboutToBeMoved, this, &MediaPlayListProxyModel::sourceRowsAboutToBeMoved);
0304         connect(playListModel, &QAbstractItemModel::rowsInserted, this, &MediaPlayListProxyModel::sourceRowsInserted);
0305         connect(playListModel, &QAbstractItemModel::rowsRemoved, this, &MediaPlayListProxyModel::sourceRowsRemoved);
0306         connect(playListModel, &QAbstractItemModel::rowsMoved, this, &MediaPlayListProxyModel::sourceRowsMoved);
0307         connect(playListModel, &QAbstractItemModel::dataChanged, this, &MediaPlayListProxyModel::sourceDataChanged);
0308         connect(playListModel, &QAbstractItemModel::headerDataChanged, this, &MediaPlayListProxyModel::sourceHeaderDataChanged);
0309         connect(playListModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &MediaPlayListProxyModel::sourceLayoutAboutToBeChanged);
0310         connect(playListModel, &QAbstractItemModel::layoutChanged, this, &MediaPlayListProxyModel::sourceLayoutChanged);
0311         connect(playListModel, &QAbstractItemModel::modelAboutToBeReset, this, &MediaPlayListProxyModel::sourceModelAboutToBeReset);
0312         connect(playListModel, &QAbstractItemModel::modelReset, this, &MediaPlayListProxyModel::sourceModelReset);
0313     }
0314 }
0315 
0316 void MediaPlayListProxyModel::setSourceModel(QAbstractItemModel *sourceModel)
0317 {
0318     QAbstractProxyModel::setSourceModel(sourceModel);
0319 }
0320 
0321 QPersistentModelIndex MediaPlayListProxyModel::previousTrack() const
0322 {
0323     return d->mPreviousTrack;
0324 }
0325 
0326 QPersistentModelIndex MediaPlayListProxyModel::currentTrack() const
0327 {
0328     return d->mCurrentTrack;
0329 }
0330 
0331 QPersistentModelIndex MediaPlayListProxyModel::nextTrack() const
0332 {
0333     return d->mNextTrack;
0334 }
0335 
0336 void MediaPlayListProxyModel::setRepeatMode(Repeat value)
0337 {
0338     if (d->mRepeatMode != value) {
0339         d->mRepeatMode = value;
0340         Q_EMIT repeatModeChanged();
0341         Q_EMIT remainingTracksChanged();
0342         Q_EMIT persistentStateChanged();
0343         determineAndNotifyPreviousAndNextTracks();
0344     }
0345 }
0346 
0347 MediaPlayListProxyModel::Repeat MediaPlayListProxyModel::repeatMode() const
0348 {
0349     return d->mRepeatMode;
0350 }
0351 
0352 void MediaPlayListProxyModel::setShufflePlayList(const bool value)
0353 {
0354     if (d->mShufflePlayList != value) {
0355         auto playListSize = d->mPlayListModel->rowCount();
0356 
0357         if (playListSize != 0) {
0358             Q_EMIT layoutAboutToBeChanged(QList<QPersistentModelIndex>(), QAbstractItemModel::VerticalSortHint);
0359             if (value) {
0360                 d->mRandomMapping.clear();
0361                 d->mRandomMapping.reserve(playListSize);
0362 
0363                 QModelIndexList to;
0364                 to.reserve(playListSize);
0365                 for (int i = 0; i < playListSize; ++i) {
0366                     to.append(index(i,0));
0367                     d->mRandomMapping.append(i);
0368                 }
0369 
0370                 QModelIndexList from;
0371                 from.reserve(playListSize);
0372                 // Adding the current track first if it is not the only one
0373                 if (playListSize > 1) {
0374                     if (currentTrackRow() != 0) {
0375                         std::swap(d->mRandomMapping[0], d->mRandomMapping[currentTrackRow()]);
0376                     }
0377                     from.append(index(d->mRandomMapping.at(0), 0));
0378                 }
0379                 // Fisher-Yates algorithm
0380                 for (int i = 1;  i < playListSize - 1; ++i) {
0381                     const int swapIndex = d->mRandomGenerator.bounded(i, playListSize);
0382                     std::swap(d->mRandomMapping[i], d->mRandomMapping[swapIndex]);
0383                     from.append(index(d->mRandomMapping.at(i), 0));
0384                 }
0385                 from.append(index(d->mRandomMapping.at(playListSize - 1), 0));
0386                 changePersistentIndexList(from, to);
0387             } else {
0388                 QModelIndexList from;
0389                 from.reserve(playListSize);
0390                 QModelIndexList to;
0391                 to.reserve(playListSize);
0392                 for (int i = 0; i < playListSize; ++i) {
0393                     to.append(index(d->mRandomMapping.at(i), 0));
0394                     from.append(index(i, 0));
0395                 }
0396                 changePersistentIndexList(from, to);
0397                 d->mRandomMapping.clear();
0398             }
0399 
0400             d->mCurrentPlayListPosition = d->mCurrentTrack.row();
0401             d->mShufflePlayList = value;
0402             Q_EMIT layoutChanged(QList<QPersistentModelIndex>(), QAbstractItemModel::VerticalSortHint);
0403             determineAndNotifyPreviousAndNextTracks();
0404         } else {
0405             d->mShufflePlayList = value;
0406         }
0407         Q_EMIT shufflePlayListChanged();
0408         Q_EMIT remainingTracksChanged();
0409         Q_EMIT persistentStateChanged();
0410     }
0411 }
0412 
0413 bool MediaPlayListProxyModel::shufflePlayList() const
0414 {
0415     return d->mShufflePlayList;
0416 }
0417 
0418 void MediaPlayListProxyModel::sourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
0419 {
0420     /*
0421      * When in random mode, rows are only inserted after
0422      * the source model is done inserting new items since
0423      * new items can be added at arbitrarily positions,
0424      * which requires a split of beginInsertRows
0425      */
0426     if (!d->mShufflePlayList) {
0427         beginInsertRows(parent, start, end);
0428     }
0429 }
0430 
0431 void MediaPlayListProxyModel::sourceRowsInserted(const QModelIndex &parent, int start, int end)
0432 {
0433     if (d->mShufflePlayList) {
0434         const auto newItemsCount = end - start + 1;
0435         d->mRandomMapping.reserve(rowCount() + newItemsCount);
0436         if (rowCount() == 0) {
0437             beginInsertRows(parent, start, end);
0438             for (int i = 0; i < newItemsCount; ++i) {
0439                 //QRandomGenerator.bounded(int) is exclusive, thus + 1
0440                 const auto random = d->mRandomGenerator.bounded(d->mRandomMapping.count()+1);
0441                 d->mRandomMapping.insert(random, start + i);
0442             }
0443             endInsertRows();
0444         } else {
0445             for (int i = 0; i < newItemsCount; ++i) {
0446                 //QRandomGenerator.bounded(int) is exclusive, thus + 1
0447                 const auto random = d->mRandomGenerator.bounded(d->mRandomMapping.count()+1);
0448                 beginInsertRows(parent, random, random);
0449                 d->mRandomMapping.insert(random, start + i);
0450                 endInsertRows();
0451             }
0452         }
0453     } else {
0454         endInsertRows();
0455     }
0456     determineTracks();
0457     Q_EMIT tracksCountChanged();
0458     Q_EMIT remainingTracksChanged();
0459     Q_EMIT persistentStateChanged();
0460 }
0461 
0462 void MediaPlayListProxyModel::sourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
0463 {
0464     if (d->mShufflePlayList) {
0465         if (end - start + 1 == rowCount()) {
0466             beginRemoveRows(parent, start, end);
0467             d->mRandomMapping.clear();
0468             endRemoveRows();
0469         }
0470         int row = 0;
0471         auto it = d->mRandomMapping.begin();
0472         while (it != d->mRandomMapping.end()) {
0473             if (*it >= start && *it <= end){
0474                 beginRemoveRows(parent, row, row);
0475                 it = d->mRandomMapping.erase(it);
0476                 endRemoveRows();
0477             } else {
0478                 if (*it > end) {
0479                     *it = *it - end + start - 1;
0480                 }
0481                 it++;
0482                 row++;
0483             }
0484         }
0485     } else {
0486         d->mCurrentTrackWasValid = d->mCurrentTrack.isValid();
0487         beginRemoveRows(parent, start, end);
0488     }
0489 }
0490 
0491 void MediaPlayListProxyModel::sourceRowsRemoved(const QModelIndex &parent, int start, int end)
0492 {
0493     Q_UNUSED(parent);
0494     Q_UNUSED(start);
0495     Q_UNUSED(end);
0496     if (!d->mShufflePlayList) {
0497         endRemoveRows();
0498     }
0499     if (!d->mCurrentTrack.isValid()) {
0500         d->mCurrentTrack = index(d->mCurrentPlayListPosition, 0);
0501         if (d->mCurrentTrack.isValid() && d->mCurrentTrackWasValid) {
0502             notifyCurrentTrackChanged();
0503         } else {
0504             if (!d->mCurrentTrack.isValid()) {
0505                 Q_EMIT playListFinished();
0506                 determineTracks();
0507                 if (!d->mCurrentTrack.isValid() && d->mCurrentTrackWasValid) {
0508                     notifyCurrentTrackChanged();
0509                 }
0510             }
0511         }
0512     }
0513     if (!d->mNextTrack.isValid() || !d->mPreviousTrack.isValid()) {
0514         determineAndNotifyPreviousAndNextTracks();
0515     }
0516     Q_EMIT tracksCountChanged();
0517     Q_EMIT remainingTracksChanged();
0518     Q_EMIT persistentStateChanged();
0519 }
0520 
0521 void MediaPlayListProxyModel::sourceRowsAboutToBeMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destParent, int destRow)
0522 {
0523     Q_ASSERT(!d->mShufflePlayList);
0524     beginMoveRows(parent, start, end, destParent, destRow);
0525 }
0526 
0527 void MediaPlayListProxyModel::sourceRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destParent, int destRow)
0528 {
0529     Q_ASSERT(!d->mShufflePlayList);
0530     Q_UNUSED(parent);
0531     Q_UNUSED(start);
0532     Q_UNUSED(end);
0533     Q_UNUSED(destParent);
0534     Q_UNUSED(destRow);
0535     endMoveRows();
0536     Q_EMIT remainingTracksChanged();
0537     Q_EMIT persistentStateChanged();
0538 }
0539 
0540 void MediaPlayListProxyModel::sourceModelAboutToBeReset()
0541 {
0542     beginResetModel();
0543 }
0544 void MediaPlayListProxyModel::sourceModelReset()
0545 {
0546     endResetModel();
0547 }
0548 
0549 void MediaPlayListProxyModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
0550 {
0551     auto startSourceRow = topLeft.row();
0552     auto endSourceRow = bottomRight.row();
0553     for (int i = startSourceRow; i <= endSourceRow; i++) {
0554         Q_EMIT dataChanged(index(mapRowFromSource(i), 0), index(mapRowFromSource(i), 0), roles);
0555         if (i == d->mCurrentTrack.row()) {
0556             Q_EMIT currentTrackDataChanged();
0557         } else if (i == d->mNextTrack.row()) {
0558             Q_EMIT nextTrackDataChanged();
0559         } else if (i == d->mPreviousTrack.row()) {
0560             Q_EMIT previousTrackDataChanged();
0561         }
0562         determineTracks();
0563     }
0564 }
0565 
0566 void MediaPlayListProxyModel::sourceLayoutAboutToBeChanged()
0567 {
0568     Q_EMIT layoutAboutToBeChanged();
0569 }
0570 
0571 void MediaPlayListProxyModel::sourceLayoutChanged()
0572 {
0573     Q_EMIT layoutChanged();
0574 }
0575 
0576 void MediaPlayListProxyModel::sourceHeaderDataChanged(Qt::Orientation orientation, int first, int last)
0577 {
0578     Q_EMIT headerDataChanged(orientation, first, last);
0579 }
0580 
0581 int MediaPlayListProxyModel::remainingTracks() const
0582 {
0583     if (!d->mCurrentTrack.isValid() || (d->mRepeatMode == Repeat::One) || (d->mRepeatMode == Repeat::Playlist)) {
0584         return -1;
0585     } else {
0586         return rowCount() - d->mCurrentTrack.row() - 1;
0587     }
0588 }
0589 
0590 int MediaPlayListProxyModel::tracksCount() const
0591 {
0592     return rowCount();
0593 }
0594 
0595 int MediaPlayListProxyModel::currentTrackRow() const
0596 {
0597     return d->mCurrentTrack.row();
0598 }
0599 
0600 void MediaPlayListProxyModel::enqueue(const DataTypes::MusicDataType &newEntry, const QString &newEntryTitle,
0601                                       ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay)
0602 {
0603     enqueue({{newEntry, newEntryTitle, {}}}, enqueueMode, triggerPlay);
0604 }
0605 
0606 void MediaPlayListProxyModel::enqueue(const QUrl &entryUrl,
0607                             ElisaUtils::PlayListEnqueueMode enqueueMode,
0608                             ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay)
0609 {
0610     enqueue({{{{DataTypes::ElementTypeRole, ElisaUtils::Track}}, {}, entryUrl}}, enqueueMode, triggerPlay);
0611 }
0612 
0613 void MediaPlayListProxyModel::enqueue(const DataTypes::EntryDataList &newEntries,
0614                             ElisaUtils::PlayListEnqueueMode enqueueMode,
0615                             ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay)
0616 {
0617     if (newEntries.isEmpty()) {
0618         return;
0619     }
0620 
0621     d->mTriggerPlay = triggerPlay;
0622 
0623     if (enqueueMode == ElisaUtils::ReplacePlayList) {
0624         if (rowCount() != 0) {
0625             clearPlayList();
0626         }
0627     }
0628 
0629     d->mPlayListModel->enqueueMultipleEntries(newEntries);
0630 }
0631 
0632 void MediaPlayListProxyModel::trackInError(const QUrl &sourceInError, QMediaPlayer::Error playerError)
0633 {
0634     d->mPlayListModel->trackInError(sourceInError, playerError);
0635 }
0636 
0637 void MediaPlayListProxyModel::skipNextTrack(ElisaUtils::SkipReason reason /*= ElisaUtils::SkipReason::Automatic*/)
0638 {
0639     if (!d->mCurrentTrack.isValid()) {
0640         return;
0641     }
0642 
0643     if (d->mRepeatMode == Repeat::One && reason == ElisaUtils::SkipReason::Automatic) {
0644         // case 1: repeat current track
0645         d->mCurrentTrack = index(d->mCurrentTrack.row(), 0);
0646     } else if (d->mCurrentTrack.row() >= rowCount() - 1) {
0647         // case 2: end of playlist, check if we loop back
0648         switch (d->mRepeatMode) {
0649         case Repeat::One:
0650         case Repeat::Playlist:
0651             d->mCurrentTrack = index(0, 0);
0652             break;
0653         case Repeat::None:
0654             d->mCurrentTrack = index(0, 0);
0655             Q_EMIT playListFinished();
0656             break;
0657         }
0658     } else {
0659         // default case: start playing next track
0660         d->mCurrentTrack = index(d->mCurrentTrack.row() + 1, 0);
0661     }
0662 
0663     notifyCurrentTrackChanged();
0664 }
0665 
0666 void MediaPlayListProxyModel::skipPreviousTrack(qint64 position)
0667 {
0668     if (!d->mCurrentTrack.isValid()) {
0669         return;
0670     }
0671 
0672     if (position > mSeekToBeginningDelay) {
0673         Q_EMIT seek(0);
0674         return;
0675     }
0676 
0677     if (d->mCurrentTrack.row() == 0) {
0678         if (d->mRepeatMode == Repeat::One || d->mRepeatMode == Repeat::Playlist) {
0679             d->mCurrentTrack = index(rowCount() - 1, 0);
0680         } else {
0681             return;
0682         }
0683     } else {
0684         d->mCurrentTrack = index(d->mCurrentTrack.row() - 1, 0);
0685     }
0686 
0687     notifyCurrentTrackChanged();
0688 }
0689 
0690 void MediaPlayListProxyModel::switchTo(int row)
0691 {
0692     if (!d->mCurrentTrack.isValid()) {
0693         return;
0694     }
0695     d->mCurrentTrack = index(row, 0);
0696 
0697     notifyCurrentTrackChanged();
0698 }
0699 
0700 void MediaPlayListProxyModel::removeSelection(QList<int> selection)
0701 {
0702     std::sort(selection.begin(), selection.end());
0703     std::reverse(selection.begin(), selection.end());
0704     for (auto oneItem : selection) {
0705         removeRow(oneItem);
0706     }
0707 }
0708 
0709 void MediaPlayListProxyModel::removeRow(int row)
0710 {
0711     d->mPlayListModel->removeRows(mapRowToSource(row), 1);
0712 }
0713 
0714 void MediaPlayListProxyModel::moveRow(int from, int to)
0715 {
0716     const bool currentTrackIndexChanged = from < to ? (from <= d->mCurrentTrack.row() && d->mCurrentTrack.row() <= to)
0717                                                     : (to <= d->mCurrentTrack.row() && d->mCurrentTrack.row() <= from);
0718 
0719     if (d->mShufflePlayList) {
0720         beginMoveRows({}, from, from, {}, from < to ? to + 1 : to);
0721         d->mRandomMapping.move(from, to);
0722         endMoveRows();
0723     } else {
0724         d->mPlayListModel->moveRows({}, from, 1, {}, from < to ? to + 1 : to);
0725     }
0726 
0727     if (currentTrackIndexChanged) {
0728         notifyCurrentTrackRowChanged();
0729     }
0730 }
0731 
0732 void MediaPlayListProxyModel::notifyCurrentTrackRowChanged()
0733 {
0734     if (d->mCurrentTrack.isValid()) {
0735         d->mCurrentPlayListPosition = d->mCurrentTrack.row();
0736     } else {
0737         d->mCurrentPlayListPosition = -1;
0738     }
0739     determineAndNotifyPreviousAndNextTracks();
0740     Q_EMIT currentTrackRowChanged();
0741     Q_EMIT remainingTracksChanged();
0742 }
0743 
0744 void MediaPlayListProxyModel::notifyCurrentTrackChanged()
0745 {
0746     notifyCurrentTrackRowChanged();
0747     Q_EMIT currentTrackChanged(d->mCurrentTrack);
0748 }
0749 
0750 void MediaPlayListProxyModel::determineAndNotifyPreviousAndNextTracks()
0751 {
0752     if (!d->mCurrentTrack.isValid()) {
0753         d->mPreviousTrack = QPersistentModelIndex();
0754         d->mNextTrack = QPersistentModelIndex();
0755     }
0756     auto mOldPreviousTrack = d->mPreviousTrack;
0757     auto mOldNextTrack = d->mNextTrack;
0758     switch (d->mRepeatMode) {
0759     case Repeat::None:
0760         // return nothing if no tracks available
0761         if (d->mCurrentTrack.row() == 0) {
0762             d->mPreviousTrack = QPersistentModelIndex();
0763         } else {
0764             d->mPreviousTrack = index(d->mCurrentTrack.row() - 1, 0);
0765         }
0766         if (d->mCurrentTrack.row() == rowCount() - 1) {
0767             d->mNextTrack = QPersistentModelIndex();
0768         } else {
0769             d->mNextTrack = index(d->mCurrentTrack.row() + 1, 0);
0770         }
0771         break;
0772     case Repeat::Playlist:
0773         // forward to end or begin when repeating
0774         if (d->mCurrentTrack.row() == 0) {
0775             d->mPreviousTrack = index(rowCount() - 1, 0);
0776         } else {
0777             d->mPreviousTrack = index(d->mCurrentTrack.row() - 1, 0);
0778         }
0779         if (d->mCurrentTrack.row() == rowCount() - 1) {
0780             d->mNextTrack = index(0, 0);
0781         } else {
0782             d->mNextTrack = index(d->mCurrentTrack.row() + 1, 0);
0783         }
0784         break;
0785     case Repeat::One:
0786         d->mPreviousTrack = index(d->mCurrentTrack.row(), 0);
0787         d->mNextTrack = index(d->mCurrentTrack.row(), 0);
0788         break;
0789     }
0790     if (d->mPreviousTrack != mOldPreviousTrack) {
0791         Q_EMIT previousTrackChanged(d->mPreviousTrack);
0792     }
0793     if (d->mNextTrack != mOldNextTrack) {
0794         Q_EMIT nextTrackChanged(d->mNextTrack);
0795     }
0796 }
0797 
0798 int MediaPlayListProxyModel::indexForTrackUrl(const QUrl &url)
0799 {
0800     for (int i = 0; i < rowCount(); ++i) {
0801         const QUrl thisTrackUrl = data(index(i,0), MediaPlayList::ResourceRole).toUrl();
0802         if (thisTrackUrl == url) {
0803             return i;
0804         }
0805     }
0806     return -1;
0807 }
0808 
0809 void MediaPlayListProxyModel::switchToTrackUrl(const QUrl &url, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay)
0810 {
0811     switchTo(indexForTrackUrl(url));
0812 
0813     if (triggerPlay == ElisaUtils::TriggerPlay) {
0814         Q_EMIT requestPlay();
0815     }
0816 }
0817 
0818 void MediaPlayListProxyModel::clearPlayList()
0819 {
0820     if (rowCount() == 0) {
0821         return;
0822     }
0823     d->mPersistentSettingsForUndo = persistentState();
0824     d->mCurrentPlayListPosition = -1;
0825     d->mCurrentTrack = QPersistentModelIndex{};
0826     notifyCurrentTrackChanged();
0827     d->mPlayListModel->clearPlayList();
0828     Q_EMIT clearPlayListPlayer();
0829     Q_EMIT displayUndoNotification();
0830 }
0831 
0832 void MediaPlayListProxyModel::undoClearPlayList()
0833 {
0834     d->mPlayListModel->clearPlayList();
0835 
0836     setPersistentState(d->mPersistentSettingsForUndo);
0837     Q_EMIT undoClearPlayListPlayer();
0838 }
0839 
0840 void MediaPlayListProxyModel::determineTracks()
0841 {
0842     if (!d->mCurrentTrack.isValid() || d->mCurrentPlayListPosition != d->mCurrentTrack.row()) {
0843         for (int row = 0; row < rowCount(); ++row) {
0844             auto candidateTrack = index(row, 0);
0845             const auto type = candidateTrack.data(MediaPlayList::ElementTypeRole).value<ElisaUtils::PlayListEntryType>();
0846             if (candidateTrack.isValid() && candidateTrack.data(MediaPlayList::IsValidRole).toBool() &&
0847                     (type == ElisaUtils::Track || type == ElisaUtils::Radio || type == ElisaUtils::FileName)) {
0848                 d->mCurrentTrack = candidateTrack;
0849                 notifyCurrentTrackChanged();
0850                 if (d->mTriggerPlay == ElisaUtils::TriggerPlay) {
0851                     d->mTriggerPlay = ElisaUtils::DoNotTriggerPlay;
0852                     Q_EMIT ensurePlay();
0853                 }
0854                 break;
0855             }
0856         }
0857     }
0858     if (!d->mNextTrack.isValid() || !d->mPreviousTrack.isValid()) {
0859         determineAndNotifyPreviousAndNextTracks();
0860     }
0861 }
0862 
0863 bool MediaPlayListProxyModel::savePlayList(const QUrl &fileName)
0864 {
0865     QFile outputFile(fileName.toLocalFile());
0866     auto open = outputFile.open(QFile::WriteOnly);
0867     if (!open) {
0868         return false;
0869     }
0870 
0871     QList<QString> listOfFilePaths;
0872 
0873     if (Elisa::ElisaConfiguration::self()->alwaysUseAbsolutePlaylistPaths()) {
0874         for (int i = 0; i < rowCount(); ++i) {
0875             if (data(index(i,0), MediaPlayList::IsValidRole).toBool()) {
0876                 data(index(i,0), MediaPlayList::ResourceRole).toUrl();
0877                 listOfFilePaths.append(data(index(i,0), MediaPlayList::ResourceRole).toUrl().toLocalFile());
0878             }
0879         }
0880     } else {
0881         for (int i = 0; i < rowCount(); ++i) {
0882             if (data(index(i,0), MediaPlayList::IsValidRole).toBool()) {
0883                 listOfFilePaths.append(data(index(i,0), MediaPlayList::ResourceRole).toUrl().toLocalFile());
0884             }
0885         }
0886     }
0887 
0888     PlaylistParser playlistParser;
0889     QString fileContent = playlistParser.toPlaylist(fileName, listOfFilePaths);
0890 
0891     outputFile.write(fileContent.toUtf8());
0892 
0893     return true;
0894 }
0895 
0896 void MediaPlayListProxyModel::loadPlayList(const QUrl &fileName)
0897 {
0898     loadPlayList(fileName, ElisaUtils::ReplacePlayList, ElisaUtils::DoNotTriggerPlay);
0899 }
0900 
0901 void MediaPlayListProxyModel::loadPlayList(const QUrl &fileName,
0902                                            ElisaUtils::PlayListEnqueueMode enqueueMode,
0903                                            ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay)
0904 {
0905     resetPartiallyLoaded();
0906 
0907     QFile inputFile(fileName.toLocalFile());
0908     bool open = inputFile.open(QFile::ReadOnly);
0909     if (!open) {
0910         Q_EMIT playListLoadFailed();
0911     }
0912 
0913     const QByteArray fileContent = inputFile.readAll();
0914 
0915     if (enqueueMode == ElisaUtils::ReplacePlayList) {
0916         clearPlayList();
0917     }
0918 
0919     d->mLoadedPlayListUrl = fileName;
0920 
0921     DataTypes::EntryDataList newTracks;
0922     QSet<QString> processedFiles{QFileInfo(inputFile).canonicalFilePath()};
0923     loadLocalPlayList(newTracks, processedFiles, fileName, fileContent);
0924 
0925     enqueue(newTracks, enqueueMode, triggerPlay);
0926 
0927     Q_EMIT persistentStateChanged();
0928     Q_EMIT playListLoaded();
0929     Q_EMIT partiallyLoadedChanged();
0930     Q_EMIT canOpenLoadedPlaylistChanged();
0931 }
0932 
0933 QVariantMap MediaPlayListProxyModel::persistentState() const
0934 {
0935     QVariantMap currentState;
0936 
0937     currentState[QStringLiteral("playList")] = d->mPlayListModel->getEntriesForRestore();
0938     currentState[QStringLiteral("currentTrack")] = d->mCurrentPlayListPosition;
0939     currentState[QStringLiteral("shufflePlayList")] = d->mShufflePlayList;
0940     currentState[QStringLiteral("repeatMode")] = d->mRepeatMode;
0941 
0942     return currentState;
0943 }
0944 
0945 void MediaPlayListProxyModel::setPersistentState(const QVariantMap &persistentStateValue)
0946 {
0947     qCDebug(orgKdeElisaPlayList()) << "MediaPlayListProxyModel::setPersistentState" << persistentStateValue;
0948 
0949     auto playListIt = persistentStateValue.find(QStringLiteral("playList"));
0950     if (playListIt != persistentStateValue.end()) {
0951         d->mPlayListModel->enqueueRestoredEntries(playListIt.value().toList());
0952     }
0953 
0954     auto playerCurrentTrack = persistentStateValue.find(QStringLiteral("currentTrack"));
0955     if (playerCurrentTrack != persistentStateValue.end()) {
0956         auto newIndex = index(playerCurrentTrack->toInt(), 0);
0957         if (newIndex.isValid() && (newIndex != d->mCurrentTrack)) {
0958             d->mCurrentTrack = newIndex;
0959             notifyCurrentTrackChanged();
0960         }
0961     }
0962 
0963     auto shufflePlayListStoredValue = persistentStateValue.find(QStringLiteral("shufflePlayList"));
0964     if (shufflePlayListStoredValue != persistentStateValue.end()) {
0965         setShufflePlayList(shufflePlayListStoredValue->toBool());
0966     }
0967 
0968     auto repeatPlayStoredValue = persistentStateValue.find(QStringLiteral("repeatPlay"));
0969     if (repeatPlayStoredValue != persistentStateValue.end() && repeatPlayStoredValue->value<bool>()) {
0970         setRepeatMode(Repeat::Playlist);
0971     }
0972 
0973     auto repeatModeStoredValue = persistentStateValue.find(QStringLiteral("repeatMode"));
0974     if (repeatModeStoredValue != persistentStateValue.end()) {
0975         setRepeatMode(repeatModeStoredValue->value<Repeat>());
0976     }
0977 
0978     Q_EMIT persistentStateChanged();
0979 }
0980 
0981 bool MediaPlayListProxyModel::partiallyLoaded() const
0982 {
0983     return d->mPartiallyLoaded;
0984 }
0985 
0986 bool MediaPlayListProxyModel::canOpenLoadedPlaylist() const
0987 {
0988 #if !KFKIO_FOUND
0989     return false;
0990 #endif
0991     return d->mLoadedPlayListUrl.isValid() && d->mLoadedPlayListUrl.isLocalFile();
0992 }
0993 
0994 void MediaPlayListProxyModel::openLoadedPlayList()
0995 {
0996 #if KFKIO_FOUND
0997     auto job = new KIO::OpenUrlJob(d->mLoadedPlayListUrl, QLatin1String("text/plain"));
0998     job->start();
0999 #endif
1000 }
1001 
1002 void MediaPlayListProxyModel::resetPartiallyLoaded()
1003 {
1004     d->mPartiallyLoaded = false;
1005 
1006     Q_EMIT partiallyLoadedChanged();
1007 }
1008 
1009 void MediaPlayListProxyModel::loadLocalFile(DataTypes::EntryDataList &newTracks, QSet<QString> &processedFiles, const QFileInfo &fileInfo)
1010 {
1011     // protection against recursion
1012     auto canonicalFilePath = fileInfo.canonicalFilePath();
1013     if (processedFiles.contains(canonicalFilePath)) {
1014         return;
1015     }
1016     processedFiles.insert(canonicalFilePath);
1017 
1018     auto fileUrl = QUrl::fromLocalFile(fileInfo.filePath());
1019     auto mimeType = d->mMimeDb.mimeTypeForUrl(fileUrl);
1020     if (fileInfo.isDir()) {
1021         if (fileInfo.isSymLink()) {
1022             return;
1023         }
1024         loadLocalDirectory(newTracks, processedFiles, fileUrl);
1025     } else {
1026         if (!mimeType.name().startsWith(QLatin1String("audio/"))) {
1027             return;
1028         }
1029         if (ElisaUtils::isPlayList(mimeType)) {
1030             QFile file(fileInfo.filePath());
1031             if (!file.open(QIODevice::ReadOnly)) {
1032                 d->mPartiallyLoaded = true;
1033                 return;
1034             }
1035             loadLocalPlayList(newTracks, processedFiles, fileUrl, file.readAll());
1036             return;
1037         }
1038         newTracks.push_back({{{DataTypes::ElementTypeRole, ElisaUtils::FileName}, {DataTypes::ResourceRole, fileUrl}}, {}, {}});
1039     }
1040 }
1041 
1042 void MediaPlayListProxyModel::loadLocalPlayList(DataTypes::EntryDataList &newTracks,
1043                                                 QSet<QString> &processedFiles,
1044                                                 const QUrl &fileName,
1045                                                 const QByteArray &fileContent)
1046 {
1047     PlaylistParser playlistParser;
1048     QList<QUrl> listOfUrls = playlistParser.fromPlaylist(fileName, fileContent);
1049 
1050     int filtered = filterLocalPlayList(listOfUrls, fileName);
1051     if (filtered != 0) {
1052         d->mPartiallyLoaded = true;
1053     }
1054 
1055     for (const QUrl &oneUrl : std::as_const(listOfUrls)) {
1056         if (oneUrl.isLocalFile()) {
1057             QFileInfo fileInfo(oneUrl.toLocalFile());
1058             loadLocalFile(newTracks, processedFiles, fileInfo);
1059         } else {
1060             newTracks.push_back({{{{DataTypes::ElementTypeRole, ElisaUtils::FileName}, {DataTypes::ResourceRole, oneUrl}}}, {}, {}});
1061         }
1062     }
1063 }
1064 
1065 void MediaPlayListProxyModel::loadLocalDirectory(DataTypes::EntryDataList &newTracks, QSet<QString> &processedFiles, const QUrl &dirName)
1066 {
1067     QDir dirInfo(dirName.toLocalFile());
1068     auto fileInfoList = dirInfo.entryInfoList(QDir::NoDotAndDotDot | QDir::Readable | QDir::Files | QDir::Dirs, QDir::Name);
1069 
1070     for (const auto &fileInfo : fileInfoList) {
1071         loadLocalFile(newTracks, processedFiles, fileInfo);
1072     }
1073 }
1074 
1075 int MediaPlayListProxyModel::filterLocalPlayList(QList<QUrl>& result, const QUrl &playlistUrl)
1076 {
1077     int filtered = 0;
1078 
1079     for (auto iterator = result.begin(); iterator != result.end();) {
1080         bool exists = true;
1081 
1082         auto &url = *iterator;
1083         if (url.isLocalFile()) {
1084             QString file = url.toLocalFile();
1085 
1086             QFileInfo fileInfo(file);
1087             if (playlistUrl.isLocalFile() && fileInfo.isRelative()) {
1088                 auto absoluteDir = QFileInfo(playlistUrl.toLocalFile()).absoluteDir();
1089                 if (fileInfo.isDir()) {
1090                     file = absoluteDir.absolutePath() + QDir::separator() + fileInfo.path();
1091                 } else {
1092                     file = absoluteDir.absoluteFilePath(file);
1093                 }
1094                 fileInfo.setFile(file);
1095                 url = QUrl::fromLocalFile(file);
1096             }
1097 
1098             exists = fileInfo.exists();
1099         }
1100 
1101         if (exists) {
1102             ++iterator;
1103         } else {
1104             ++filtered;
1105             iterator = result.erase(iterator);
1106         }
1107     }
1108 
1109     return filtered;
1110 }
1111 
1112 #include "moc_mediaplaylistproxymodel.cpp"