File indexing completed on 2025-02-23 04:35:16

0001 // SPDX-FileCopyrightText: 2019 Linus Jahn <lnj@kaidan.im>
0002 // SPDX-License-Identifier: GPL-3.0-or-later
0003 
0004 #include "videomodel.h"
0005 
0006 #include "plasmatube.h"
0007 #include "videolistmodel.h"
0008 
0009 #include <QFutureWatcher>
0010 #include <QJsonArray>
0011 #include <QJsonDocument>
0012 #include <QProcess>
0013 
0014 using namespace Qt::StringLiterals;
0015 
0016 VideoModel::VideoModel(QObject *parent)
0017     : QObject(parent)
0018     , m_video(new VideoItem(this))
0019 {
0020     connect(this, &VideoModel::videoIdChanged, this, [this] {
0021         m_remoteUrl.clear();
0022         m_formatUrl.clear();
0023         Q_EMIT remoteUrlChanged();
0024     });
0025 }
0026 
0027 void VideoModel::fetch(const QString &videoId)
0028 {
0029     m_videoId = videoId;
0030 
0031     // if currently loading, abort
0032     if (m_watcher) {
0033         m_watcher->cancel();
0034         m_watcher->deleteLater();
0035         m_watcher = nullptr;
0036     }
0037 
0038     // clean up
0039     m_video->deleteLater();
0040     m_video = new VideoItem(this);
0041     Q_EMIT videoChanged();
0042 
0043     auto future = PlasmaTube::instance().sourceManager()->selectedSource()->api()->requestVideo(m_videoId);
0044 
0045     m_watcher = new QFutureWatcher<QInvidious::VideoResult>(this);
0046     connect(m_watcher, &QFutureWatcherBase::finished, this, [=] {
0047         auto result = m_watcher->result();
0048 
0049         if (const auto video = std::get_if<QInvidious::Video>(&result)) {
0050             m_video->deleteLater();
0051             m_video = new VideoItem(*video, this);
0052             Q_EMIT videoChanged();
0053         } else if (const auto error = std::get_if<QInvidious::Error>(&result)) {
0054             qDebug() << "VideoModel::fetch(): Error:" << error->second << error->first;
0055             Q_EMIT errorOccurred(error->second);
0056         }
0057 
0058         m_watcher->deleteLater();
0059         m_watcher = nullptr;
0060         Q_EMIT isLoadingChanged();
0061     });
0062     m_watcher->setFuture(future);
0063     Q_EMIT isLoadingChanged();
0064 
0065     // load format list
0066     QString youtubeDl = QStringLiteral("yt-dlp");
0067     QStringList arguments;
0068     arguments << QLatin1String("--dump-json") << m_videoId;
0069     auto process = new QProcess();
0070     process->setReadChannel(QProcess::StandardOutput);
0071     process->start(youtubeDl, arguments);
0072 
0073     connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [=](int, QProcess::ExitStatus) {
0074         const auto doc = QJsonDocument::fromJson(process->readAllStandardOutput());
0075         const auto formatsArray = doc.object()[QLatin1String("formats")].toArray();
0076         for (const auto &value : formatsArray) {
0077             const auto format = value.toObject();
0078             const auto formatNote = format["format_note"_L1].toString();
0079             if (formatNote == "medium"_L1) {
0080                 m_audioUrl = format["url"_L1].toString();
0081             } else {
0082                 m_formatUrl[formatNote] = format["url"_L1].toString();
0083             }
0084         }
0085         Q_EMIT remoteUrlChanged();
0086         Q_EMIT formatListChanged();
0087         process->deleteLater();
0088     });
0089 }
0090 
0091 bool VideoModel::isLoading() const
0092 {
0093     return m_watcher != nullptr;
0094 }
0095 
0096 VideoItem::VideoItem(QObject *parent)
0097     : QObject(parent)
0098     , m_isLoaded(false)
0099 {
0100 }
0101 
0102 VideoItem::VideoItem(const QInvidious::Video &video, QObject *parent)
0103     : QObject(parent)
0104     , m_isLoaded(true)
0105 {
0106     *static_cast<QInvidious::Video *>(this) = video;
0107 }
0108 
0109 bool VideoItem::isLoaded() const
0110 {
0111     return m_isLoaded;
0112 }
0113 
0114 QUrl VideoItem::thumbnailUrl(const QString &quality) const
0115 {
0116     const QUrl thumbnailUrl = thumbnail(quality).url();
0117 
0118     if (!thumbnailUrl.isEmpty() && thumbnailUrl.isRelative()) {
0119         return QUrl(PlasmaTube::instance().sourceManager()->selectedSource()->api()->apiHost() + thumbnailUrl.toString(QUrl::FullyEncoded));
0120     }
0121 
0122     return thumbnailUrl;
0123 }
0124 
0125 QUrl VideoItem::authorThumbnail(quint32 size) const
0126 {
0127     // thumbnails are sorted by size
0128     const auto authorThumbs = authorThumbnails();
0129     for (const auto &thumb : authorThumbs) {
0130         if (thumb.width() >= size)
0131             return thumb.url();
0132     }
0133     if (!authorThumbs.isEmpty())
0134         return authorThumbs.last().url();
0135     return {};
0136 }
0137 
0138 VideoListModel *VideoItem::recommendedVideosModel()
0139 {
0140     return new VideoListModel(recommendedVideos(), this);
0141 }
0142 
0143 QString VideoModel::remoteUrl()
0144 {
0145     if (!m_formatUrl.isEmpty() && m_formatUrl.contains(m_selectedFormat)) {
0146         return m_formatUrl[m_selectedFormat];
0147     }
0148     return {};
0149 }
0150 
0151 QString VideoModel::audioUrl() const
0152 {
0153     return m_audioUrl;
0154 }
0155 
0156 QStringList VideoModel::formatList() const
0157 {
0158     return m_formatUrl.keys();
0159 }
0160 
0161 QString VideoModel::selectedFormat() const
0162 {
0163     return m_selectedFormat;
0164 }
0165 
0166 void VideoModel::setSelectedFormat(const QString &selectedFormat)
0167 {
0168     if (m_selectedFormat == selectedFormat) {
0169         return;
0170     }
0171     m_selectedFormat = selectedFormat;
0172     Q_EMIT remoteUrlChanged();
0173     Q_EMIT selectedFormatChanged();
0174 }
0175 
0176 VideoItem *VideoModel::video() const
0177 {
0178     return m_video;
0179 }
0180 
0181 QString VideoModel::videoId() const
0182 {
0183     return m_videoId;
0184 }
0185 
0186 void VideoModel::clearVideo()
0187 {
0188     if (m_video) {
0189         m_video->deleteLater();
0190     }
0191 
0192     m_video = new VideoItem(this);
0193     Q_EMIT videoChanged();
0194 }
0195 
0196 #include "moc_videomodel.cpp"