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"