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"