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"