File indexing completed on 2024-05-05 04:50:49
0001 /* 0002 * SPDX-FileCopyrightText: 2020 George Florea Bănuș <georgefb899@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 */ 0006 0007 #include "playlistmodel.h" 0008 0009 #include <KFileItem> 0010 #include <KFileMetaData/Properties> 0011 #include <KIO/DeleteOrTrashJob> 0012 #include <KIO/OpenFileManagerWindowJob> 0013 #include <KIO/RenameFileDialog> 0014 #include <kio_version.h> 0015 0016 #include <QClipboard> 0017 #include <QCollator> 0018 #include <QDirIterator> 0019 #include <QFile> 0020 #include <QFileInfo> 0021 #include <QJsonArray> 0022 #include <QJsonDocument> 0023 #include <QJsonValue> 0024 #include <QProcess> 0025 0026 #include "application.h" 0027 #include "generalsettings.h" 0028 #include "playlistsettings.h" 0029 #include "worker.h" 0030 0031 PlaylistModel::PlaylistModel(QObject *parent) 0032 : QAbstractListModel(parent) 0033 { 0034 connect(this, &PlaylistModel::itemAdded, Worker::instance(), &Worker::getMetaData); 0035 0036 connect(Worker::instance(), &Worker::metaDataReady, this, [=](int i, KFileMetaData::PropertyMultiMap metaData) { 0037 if (m_playlist.isEmpty()) { 0038 return; 0039 } 0040 auto duration = metaData.value(KFileMetaData::Property::Duration).toInt(); 0041 auto title = metaData.value(KFileMetaData::Property::Title).toString(); 0042 0043 m_playlist[i].duration = Application::formatTime(duration); 0044 m_playlist[i].mediaTitle = title; 0045 0046 Q_EMIT dataChanged(index(i, 0), index(i, 0)); 0047 }); 0048 } 0049 0050 int PlaylistModel::rowCount(const QModelIndex &parent) const 0051 { 0052 Q_UNUSED(parent) 0053 return m_playlist.count(); 0054 } 0055 0056 QVariant PlaylistModel::data(const QModelIndex &index, int role) const 0057 { 0058 if (!index.isValid()) { 0059 return QVariant(); 0060 } 0061 0062 auto item = m_playlist.at(index.row()); 0063 switch (role) { 0064 case NameRole: 0065 return QVariant(item.filename); 0066 case TitleRole: 0067 return item.mediaTitle.isEmpty() ? QVariant(item.filename) : QVariant(item.mediaTitle); 0068 case PathRole: 0069 return QVariant(item.url); 0070 case DurationRole: 0071 return QVariant(item.duration); 0072 case PlayingRole: 0073 return QVariant(m_playingItem == index.row()); 0074 case FolderPathRole: 0075 return QVariant(item.folderPath); 0076 case IsLocalRole: 0077 return QVariant(!item.url.scheme().startsWith(QStringLiteral("http"))); 0078 } 0079 0080 return QVariant(); 0081 } 0082 0083 QHash<int, QByteArray> PlaylistModel::roleNames() const 0084 { 0085 QHash<int, QByteArray> roles = { 0086 {NameRole, "name"}, 0087 {TitleRole, "title"}, 0088 {PathRole, "path"}, 0089 {FolderPathRole, "folderPath"}, 0090 {DurationRole, "duration"}, 0091 {PlayingRole, "isPlaying"}, 0092 {IsLocalRole, "isLocal"}, 0093 }; 0094 return roles; 0095 } 0096 0097 void PlaylistModel::clear() 0098 { 0099 m_playlistPath = QString(); 0100 m_playingItem = -1; 0101 beginResetModel(); 0102 m_playlist.clear(); 0103 endResetModel(); 0104 } 0105 0106 void PlaylistModel::addItem(const QString &path, Behaviour behaviour) 0107 { 0108 auto url = QUrl::fromUserInput(path); 0109 addItem(url, behaviour); 0110 } 0111 0112 void PlaylistModel::addItem(const QUrl &url, Behaviour behaviour) 0113 { 0114 if (!url.isValid() || url.isEmpty()) { 0115 return; 0116 } 0117 if (behaviour == Behaviour::Clear) { 0118 clear(); 0119 } 0120 0121 if (url.scheme() == QStringLiteral("file")) { 0122 auto mimeType = Application::mimeType(url); 0123 0124 if (mimeType == QStringLiteral("audio/x-mpegurl")) { 0125 m_playlistPath = url.toString(); 0126 addM3uItems(url); 0127 return; 0128 } 0129 0130 if (isVideoOrAudioMimeType(mimeType)) { 0131 if (behaviour == Behaviour::Clear) { 0132 if (PlaylistSettings::loadSiblings()) { 0133 getSiblingItems(url); 0134 } else { 0135 appendItem(url); 0136 setPlayingItem(0); 0137 } 0138 } else { 0139 appendItem(url); 0140 } 0141 0142 return; 0143 } 0144 } 0145 0146 if (url.scheme() == QStringLiteral("http") || url.scheme() == QStringLiteral("https")) { 0147 if (Application::isYoutubePlaylist(url.toString())) { 0148 m_playlistPath = url.toString(); 0149 getYouTubePlaylist(url, behaviour); 0150 } else { 0151 if (behaviour == Behaviour::Clear) { 0152 appendItem(url); 0153 setPlayingItem(0); 0154 } else { 0155 appendItem(url); 0156 } 0157 } 0158 } 0159 } 0160 0161 void PlaylistModel::appendItem(const QUrl &url) 0162 { 0163 PlaylistItem item; 0164 QFileInfo itemInfo(url.toLocalFile()); 0165 auto row{m_playlist.count()}; 0166 if (itemInfo.exists() && itemInfo.isFile()) { 0167 item.url = url; 0168 item.filename = itemInfo.fileName(); 0169 item.mediaTitle = QString(); 0170 item.folderPath = itemInfo.absolutePath(); 0171 item.duration = QString(); 0172 } else { 0173 if (url.scheme().startsWith(QStringLiteral("http"))) { 0174 item.url = url; 0175 item.filename = url.toString(); 0176 // causes issues with lots of links 0177 if (m_httpItemCounter < 20) { 0178 getHttpItemInfo(url, row); 0179 ++m_httpItemCounter; 0180 } 0181 } 0182 } 0183 0184 if (item.url.isEmpty()) { 0185 return; 0186 } 0187 0188 beginInsertRows(QModelIndex(), m_playlist.count(), m_playlist.count()); 0189 0190 m_playlist.append(item); 0191 Q_EMIT itemAdded(row, item.url.toString()); 0192 0193 endInsertRows(); 0194 } 0195 0196 void PlaylistModel::getSiblingItems(const QUrl &url) 0197 { 0198 QFileInfo openedFileInfo(url.toLocalFile()); 0199 if (!openedFileInfo.exists() || !openedFileInfo.isFile()) { 0200 return; 0201 } 0202 0203 QStringList siblingFiles; 0204 QDirIterator it(openedFileInfo.absolutePath(), QDir::Files, QDirIterator::NoIteratorFlags); 0205 while (it.hasNext()) { 0206 QString siblingFile = it.next(); 0207 QFileInfo siblingFileInfo(siblingFile); 0208 auto siblingUrl = QUrl::fromLocalFile(siblingFile); 0209 QString mimeType = Application::mimeType(siblingUrl); 0210 if (!siblingFileInfo.exists()) { 0211 continue; 0212 } 0213 if (isVideoOrAudioMimeType(mimeType)) { 0214 siblingFiles.append(siblingFileInfo.absoluteFilePath()); 0215 } 0216 } 0217 0218 QCollator collator; 0219 collator.setNumericMode(true); 0220 std::sort(siblingFiles.begin(), siblingFiles.end(), collator); 0221 0222 beginInsertRows(QModelIndex(), 0, siblingFiles.count() - 1); 0223 for (const auto &file : siblingFiles) { 0224 QFileInfo fileInfo(file); 0225 auto fileUrl = QUrl::fromLocalFile(file); 0226 PlaylistItem item; 0227 item.url = fileUrl; 0228 item.filename = fileInfo.fileName(); 0229 item.mediaTitle = QString(); 0230 item.folderPath = fileInfo.absolutePath(); 0231 item.duration = QString(); 0232 m_playlist.append(item); 0233 if (url == fileUrl) { 0234 setPlayingItem(m_playlist.count() - 1); 0235 } 0236 Q_EMIT itemAdded(m_playlist.count() - 1, item.url.toString()); 0237 } 0238 endInsertRows(); 0239 } 0240 0241 void PlaylistModel::addM3uItems(const QUrl &url) 0242 { 0243 if (url.scheme() != QStringLiteral("file") || Application::mimeType(url) != QStringLiteral("audio/x-mpegurl")) { 0244 return; 0245 } 0246 0247 QFile m3uFile(url.toString(QUrl::PreferLocalFile)); 0248 if (!m3uFile.open(QFile::ReadOnly)) { 0249 qDebug() << "can't open playlist file"; 0250 return; 0251 } 0252 0253 int i{0}; 0254 bool matchFound{false}; 0255 while (!m3uFile.atEnd()) { 0256 QByteArray line = QByteArray::fromPercentEncoding(m3uFile.readLine().simplified()); 0257 // ignore comments 0258 if (line.startsWith("#")) { 0259 continue; 0260 } 0261 0262 auto url = QUrl::fromUserInput(QString::fromUtf8(line)); 0263 if (!url.scheme().isEmpty()) { 0264 addItem(url, Behaviour::Append); 0265 } else { 0266 // figure out if it's a relative path 0267 url = QUrl::fromUserInput(QString::fromUtf8(line), QFileInfo(m3uFile).absolutePath()); 0268 addItem(url, Behaviour::Append); 0269 } 0270 0271 if (!matchFound && url == QUrl::fromUserInput(GeneralSettings::lastPlayedFile())) { 0272 setPlayingItem(i); 0273 matchFound = true; 0274 } 0275 ++i; 0276 } 0277 m3uFile.close(); 0278 0279 if (!matchFound) { 0280 setPlayingItem(0); 0281 } 0282 } 0283 0284 void PlaylistModel::getYouTubePlaylist(const QUrl &url, Behaviour behaviour) 0285 { 0286 // use youtube-dl to get the required playlist info as json 0287 auto ytdlProcess = new QProcess(); 0288 auto args = QStringList() << QStringLiteral("-J") << QStringLiteral("--flat-playlist") << url.toString(); 0289 ytdlProcess->setProgram(Application::youtubeDlExecutable()); 0290 ytdlProcess->setArguments(args); 0291 ytdlProcess->start(); 0292 0293 connect(ytdlProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [=](int, QProcess::ExitStatus) { 0294 QString json = QString::fromUtf8(ytdlProcess->readAllStandardOutput()); 0295 QJsonValue entries = QJsonDocument::fromJson(json.toUtf8())[QStringLiteral("entries")]; 0296 QString playlistTitle = QJsonDocument::fromJson(json.toUtf8())[QStringLiteral("title")].toString(); 0297 if (entries.toArray().isEmpty()) { 0298 return; 0299 } 0300 0301 bool matchFound{false}; 0302 for (int i = 0; i < entries.toArray().size(); ++i) { 0303 auto id = entries[i][QStringLiteral("id")].toString(); 0304 auto url = QStringLiteral("https://youtu.be/%1").arg(entries[i][QStringLiteral("id")].toString()); 0305 auto title = entries[i][QStringLiteral("title")].toString(); 0306 auto duration = entries[i][QStringLiteral("duration")].toDouble(); 0307 0308 PlaylistItem item; 0309 item.url = QUrl::fromUserInput(url); 0310 item.filename = !title.isEmpty() ? title : url; 0311 item.mediaTitle = !title.isEmpty() ? title : url; 0312 item.folderPath = QString(); 0313 item.duration = Application::formatTime(duration); 0314 0315 beginInsertRows(QModelIndex(), m_playlist.count(), m_playlist.count()); 0316 m_playlist.append(item); 0317 Q_EMIT itemAdded(i, item.url.toString()); 0318 endInsertRows(); 0319 0320 if (GeneralSettings::lastPlayedFile().contains(id) && behaviour == Behaviour::Clear) { 0321 setPlayingItem(i); 0322 matchFound = true; 0323 } 0324 } 0325 0326 if (!matchFound && behaviour == Behaviour::Clear) { 0327 setPlayingItem(0); 0328 } 0329 }); 0330 } 0331 0332 void PlaylistModel::getHttpItemInfo(const QUrl &url, int row) 0333 { 0334 auto ytdlProcess = new QProcess(); 0335 ytdlProcess->setProgram(Application::youtubeDlExecutable()); 0336 ytdlProcess->setArguments(QStringList() << QStringLiteral("-j") << url.toString()); 0337 ytdlProcess->start(); 0338 0339 connect(ytdlProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [=](int, QProcess::ExitStatus) { 0340 QString json = QString::fromUtf8(ytdlProcess->readAllStandardOutput()); 0341 QString title = QJsonDocument::fromJson(json.toUtf8())[QStringLiteral("title")].toString(); 0342 int duration = QJsonDocument::fromJson(json.toUtf8())[QStringLiteral("duration")].toInt(); 0343 if (title.isEmpty()) { 0344 // todo: log if can't get title 0345 return; 0346 } 0347 m_playlist[row].mediaTitle = title; 0348 m_playlist[row].filename = title; 0349 m_playlist[row].duration = Application::formatTime(duration); 0350 0351 Q_EMIT dataChanged(index(row, 0), index(row, 0)); 0352 }); 0353 } 0354 0355 bool PlaylistModel::isVideoOrAudioMimeType(const QString &mimeType) 0356 { 0357 // clang-format off 0358 return (mimeType.startsWith(QStringLiteral("video/")) 0359 || mimeType.startsWith(QStringLiteral("audio/")) 0360 || mimeType == QStringLiteral("application/vnd.rn-realmedia")) 0361 && mimeType != QStringLiteral("audio/x-mpegurl"); 0362 // clang-format on 0363 } 0364 0365 void PlaylistModel::setPlayingItem(int i) 0366 { 0367 if (i == -1) { 0368 return; 0369 } 0370 0371 int previousItem = m_playingItem; 0372 m_playingItem = i; 0373 Q_EMIT dataChanged(index(previousItem, 0), index(previousItem, 0)); 0374 Q_EMIT dataChanged(index(i, 0), index(i, 0)); 0375 Q_EMIT playingItemChanged(); 0376 0377 GeneralSettings::setLastPlayedFile(m_playlist[i].url.toString()); 0378 GeneralSettings::setLastPlaylist(m_playlistPath); 0379 GeneralSettings::self()->save(); 0380 } 0381 0382 PlaylistProxyModel::PlaylistProxyModel(QObject *parent) 0383 : QSortFilterProxyModel(parent) 0384 { 0385 setDynamicSortFilter(true); 0386 } 0387 0388 void PlaylistProxyModel::sortItems(Sort sortMode) 0389 { 0390 switch (sortMode) { 0391 case Sort::NameAscending: { 0392 setSortRole(PlaylistModel::NameRole); 0393 sort(0, Qt::AscendingOrder); 0394 break; 0395 } 0396 case Sort::NameDescending: { 0397 setSortRole(PlaylistModel::NameRole); 0398 sort(0, Qt::DescendingOrder); 0399 break; 0400 } 0401 case Sort::DurationAscending: { 0402 setSortRole(PlaylistModel::DurationRole); 0403 sort(0, Qt::AscendingOrder); 0404 break; 0405 } 0406 case Sort::DurationDescending: { 0407 setSortRole(PlaylistModel::DurationRole); 0408 sort(0, Qt::DescendingOrder); 0409 break; 0410 } 0411 } 0412 } 0413 0414 int PlaylistProxyModel::getPlayingItem() 0415 { 0416 auto model = qobject_cast<PlaylistModel *>(sourceModel()); 0417 return mapFromSource(model->index(model->m_playingItem, 0)).row(); 0418 } 0419 0420 void PlaylistProxyModel::setPlayingItem(int i) 0421 { 0422 auto model = qobject_cast<PlaylistModel *>(sourceModel()); 0423 model->setPlayingItem(mapToSource(index(i, 0)).row()); 0424 } 0425 0426 void PlaylistProxyModel::playNext() 0427 { 0428 auto model = qobject_cast<PlaylistModel *>(sourceModel()); 0429 0430 auto currentIndex = mapFromSource(model->index(model->m_playingItem, 0)).row(); 0431 auto nextIndex = currentIndex + 1; 0432 0433 if (nextIndex < rowCount()) { 0434 model->setPlayingItem(mapToSource(index(nextIndex, 0)).row()); 0435 } else { 0436 setPlayingItem(0); 0437 } 0438 } 0439 0440 void PlaylistProxyModel::playPrevious() 0441 { 0442 auto model = qobject_cast<PlaylistModel *>(sourceModel()); 0443 0444 auto currentIndex = mapFromSource(model->index(model->m_playingItem, 0)).row(); 0445 auto previousIndex = currentIndex - 1; 0446 0447 if (previousIndex >= 0) { 0448 model->setPlayingItem(mapToSource(index(previousIndex, 0)).row()); 0449 } 0450 } 0451 0452 void PlaylistProxyModel::saveM3uFile(const QString &path) 0453 { 0454 QUrl url(path); 0455 QFile m3uFile(url.toString(QUrl::PreferLocalFile)); 0456 if (!m3uFile.open(QFile::WriteOnly)) { 0457 return; 0458 } 0459 for (int i{0}; i < rowCount(); ++i) { 0460 QString itemPath = data(index(i, 0), PlaylistModel::PathRole).toString(); 0461 m3uFile.write(itemPath.toUtf8().append("\n")); 0462 } 0463 m3uFile.close(); 0464 } 0465 0466 void PlaylistProxyModel::highlightInFileManager(int row) 0467 { 0468 QString path = data(index(row, 0), PlaylistModel::PathRole).toString(); 0469 KIO::highlightInFileManager({QUrl::fromUserInput(path)}); 0470 } 0471 0472 void PlaylistProxyModel::removeItem(int row) 0473 { 0474 auto model = qobject_cast<PlaylistModel *>(sourceModel()); 0475 0476 auto sourceRow = mapFromSource(model->index(row)).row(); 0477 0478 beginRemoveRows(QModelIndex(), sourceRow, sourceRow); 0479 model->m_playlist.removeAt(sourceRow); 0480 endRemoveRows(); 0481 } 0482 0483 void PlaylistProxyModel::renameFile(int row) 0484 { 0485 QString path = data(index(row, 0), PlaylistModel::PathRole).toString(); 0486 QUrl url(path); 0487 if (url.scheme().isEmpty()) { 0488 url.setScheme(QStringLiteral("file")); 0489 } 0490 KFileItem item(url); 0491 auto renameDialog = new KIO::RenameFileDialog(KFileItemList({item}), nullptr); 0492 renameDialog->open(); 0493 0494 connect(renameDialog, &KIO::RenameFileDialog::renamingFinished, this, [=](const QList<QUrl> &urls) { 0495 auto model = qobject_cast<PlaylistModel *>(sourceModel()); 0496 auto sourceRow = mapToSource(index(row, 0)).row(); 0497 auto item = model->m_playlist.at(sourceRow); 0498 item.url = QUrl::fromUserInput(urls.first().path()); 0499 item.filename = urls.first().fileName(); 0500 0501 Q_EMIT dataChanged(index(row, 0), index(row, 0)); 0502 }); 0503 } 0504 0505 void PlaylistProxyModel::trashFile(int row) 0506 { 0507 QList<QUrl> urls; 0508 QString path = data(index(row, 0), PlaylistModel::PathRole).toString(); 0509 QUrl url(path); 0510 if (url.scheme().isEmpty()) { 0511 url.setScheme(QStringLiteral("file")); 0512 } 0513 urls << url; 0514 auto *job = new KIO::DeleteOrTrashJob(urls, KIO::AskUserActionInterface::Trash, KIO::AskUserActionInterface::DefaultConfirmation, this); 0515 job->start(); 0516 0517 connect(job, &KJob::result, this, [=]() { 0518 if (job->error() == 0) { 0519 auto model = qobject_cast<PlaylistModel *>(sourceModel()); 0520 auto sourceRow = mapToSource(index(row, 0)).row(); 0521 beginRemoveRows(QModelIndex(), sourceRow, sourceRow); 0522 model->m_playlist.removeAt(sourceRow); 0523 endRemoveRows(); 0524 } 0525 }); 0526 } 0527 0528 void PlaylistProxyModel::copyFileName(int row) 0529 { 0530 auto model = qobject_cast<PlaylistModel *>(sourceModel()); 0531 auto item = model->m_playlist.at(row); 0532 QGuiApplication::clipboard()->setText(item.filename); 0533 } 0534 0535 void PlaylistProxyModel::copyFilePath(int row) 0536 { 0537 auto model = qobject_cast<PlaylistModel *>(sourceModel()); 0538 auto item = model->m_playlist.at(row); 0539 QGuiApplication::clipboard()->setText(item.url.toString()); 0540 } 0541 0542 QString PlaylistProxyModel::getFilePath(int row) 0543 { 0544 auto model = qobject_cast<PlaylistModel *>(sourceModel()); 0545 auto item = model->m_playlist.at(row); 0546 return item.url.toString(); 0547 } 0548 0549 #include "moc_playlistmodel.cpp"