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 "videolistmodel.h" 0005 0006 #include "plasmatube.h" 0007 0008 #include <KLocalizedString> 0009 0010 #include <QFutureWatcher> 0011 #include <QNetworkReply> 0012 #include <QtConcurrent> 0013 0014 QString VideoListModel::queryTypeString(QueryType type) 0015 { 0016 switch (type) { 0017 case Feed: 0018 return i18n("Subscriptions"); 0019 case Top: 0020 return i18n("Popular"); 0021 case Trending: 0022 return i18nc("@action:button All trending videos", "All"); 0023 case TrendingGaming: 0024 return i18nc("@action:button Trending gaming videos", "Gaming"); 0025 case TrendingMovies: 0026 return i18nc("@action:button Trending movie videos", "Movies"); 0027 case TrendingMusic: 0028 return i18nc("@action:button Trending music videos", "Music"); 0029 case TrendingNews: 0030 return i18nc("@action:button Trending news videos", "News"); 0031 case History: 0032 return i18nc("@action:button Video watch history", "History"); 0033 default: 0034 return {}; 0035 } 0036 } 0037 0038 QString VideoListModel::queryTypeIcon(QueryType type) 0039 { 0040 switch (type) { 0041 case Feed: 0042 return QStringLiteral("feed-subscribe"); 0043 case Top: 0044 return QStringLiteral("arrow-up-double"); 0045 case Trending: 0046 // should actually be "user-trash-full-symbolic" 0047 return QStringLiteral("favorite"); 0048 case TrendingGaming: 0049 return QStringLiteral("folder-games-symbolic"); 0050 case TrendingMovies: 0051 return QStringLiteral("folder-videos-symbolic"); 0052 case TrendingMusic: 0053 return QStringLiteral("folder-music-symbolic"); 0054 case TrendingNews: 0055 return QStringLiteral("news-subscribe"); 0056 case History: 0057 return QStringLiteral("view-history"); 0058 default: 0059 return {}; 0060 } 0061 } 0062 0063 VideoListModel::VideoListModel(QObject *parent) 0064 : AbstractListModel(parent) 0065 { 0066 } 0067 0068 VideoListModel::VideoListModel(const QList<QInvidious::VideoBasicInfo> &list, QObject *parent) 0069 : AbstractListModel(parent) 0070 , m_results(list) 0071 { 0072 } 0073 0074 int VideoListModel::rowCount(const QModelIndex &parent) const 0075 { 0076 if (parent.isValid()) 0077 return 0; 0078 return static_cast<int>(m_results.size()); 0079 } 0080 0081 QVariant VideoListModel::data(const QModelIndex &index, int role) const 0082 { 0083 if (!index.isValid() || index.parent().isValid()) 0084 return {}; 0085 0086 const QInvidious::VideoBasicInfo &video = m_results.at(index.row()); 0087 switch (role) { 0088 case IdRole: 0089 return video.videoId(); 0090 case TypeRole: 0091 return QStringLiteral("video"); 0092 case TitleRole: 0093 return video.title(); 0094 case ThumbnailRole: { 0095 const auto thumbnailUrl = video.thumbnail(QStringLiteral("medium")).url(); 0096 if (thumbnailUrl.isRelative()) { 0097 return QUrl(PlasmaTube::instance().sourceManager()->selectedSource()->api()->apiHost() + thumbnailUrl.toString(QUrl::FullyEncoded)); 0098 } 0099 return thumbnailUrl; 0100 } 0101 case LengthRole: 0102 return video.length(); 0103 case ViewCountRole: 0104 return video.viewCount(); 0105 case AuthorRole: 0106 return video.author(); 0107 case AuthorIdRole: 0108 return video.authorId(); 0109 case AuthorUrlRole: 0110 return video.authorUrl(); 0111 case PublishedRole: 0112 return video.published(); 0113 case PublishedTextRole: 0114 return video.publishedText(); 0115 case DescriptionRole: 0116 return video.description(); 0117 case DescriptionHtmlRole: 0118 return video.descriptionHtml(); 0119 case LiveNowRole: 0120 return video.liveNow(); 0121 case PaidRole: 0122 return video.paid(); 0123 case PremiumRole: 0124 return video.premium(); 0125 case WatchedRole: 0126 return PlasmaTube::instance().selectedSource()->isVideoWatched(video.videoId()); 0127 default: 0128 break; 0129 } 0130 return {}; 0131 } 0132 0133 void VideoListModel::fetchMore(const QModelIndex &index) 0134 { 0135 if (canFetchMore(index)) { 0136 switch (m_queryType) { 0137 case Feed: { 0138 auto future = PlasmaTube::instance().sourceManager()->selectedSource()->api()->requestFeed(++m_currentPage); 0139 handleQuery(future, Feed, false); 0140 break; 0141 } 0142 case Channel: { 0143 auto future = PlasmaTube::instance().sourceManager()->selectedSource()->api()->requestChannel(m_channel, ++m_currentPage); 0144 handleQuery(future, Channel, false); 0145 break; 0146 } 0147 case History: { 0148 m_currentPage++; 0149 requestQuery(History); 0150 } break; 0151 default: { 0152 } 0153 } 0154 } 0155 } 0156 0157 bool VideoListModel::canFetchMore(const QModelIndex &) const 0158 { 0159 return !m_historyPageWatcher && !m_futureWatcher && (m_queryType == Search || m_queryType == Channel || m_queryType == Feed || m_queryType == History); 0160 } 0161 0162 QString VideoListModel::title() const 0163 { 0164 switch (m_queryType) { 0165 case Search: 0166 return i18n("Search results for \"%1\"").arg(m_searchParameters.query()); 0167 default: 0168 return queryTypeString(m_queryType); 0169 } 0170 } 0171 0172 void VideoListModel::requestChannel(const QString &ucid) 0173 { 0174 m_channel = ucid; 0175 m_currentPage = 1; 0176 handleQuery(PlasmaTube::instance().sourceManager()->selectedSource()->api()->requestChannel(ucid, m_currentPage), Channel); 0177 } 0178 0179 void VideoListModel::requestPlaylist(const QString &id) 0180 { 0181 m_playlist = id; 0182 m_currentPage = 1; 0183 handleQuery(PlasmaTube::instance().sourceManager()->selectedSource()->api()->requestPlaylist(id), Playlist); 0184 } 0185 0186 void VideoListModel::requestQuery(QueryType type) 0187 { 0188 auto selectedSource = PlasmaTube::instance().selectedSource(); 0189 if (selectedSource == nullptr) { 0190 return; 0191 } 0192 0193 m_searchParameters.clear(); 0194 switch (type) { 0195 case Feed: 0196 handleQuery(selectedSource->api()->requestFeed(), type); 0197 break; 0198 case Top: 0199 handleQuery(selectedSource->api()->requestTop(), type); 0200 break; 0201 case Trending: 0202 handleQuery(selectedSource->api()->requestTrending(), type); 0203 break; 0204 case TrendingGaming: 0205 handleQuery(selectedSource->api()->requestTrending(QInvidious::Gaming), type); 0206 break; 0207 case TrendingMovies: 0208 handleQuery(selectedSource->api()->requestTrending(QInvidious::Movies), type); 0209 break; 0210 case TrendingMusic: 0211 handleQuery(selectedSource->api()->requestTrending(QInvidious::Music), type); 0212 break; 0213 case TrendingNews: 0214 handleQuery(selectedSource->api()->requestTrending(QInvidious::News), type); 0215 break; 0216 case History: 0217 requestHistory(); 0218 break; 0219 default: 0220 qDebug() << "VideoListModel::requestQuery() called with not allowed type" << type; 0221 } 0222 } 0223 0224 void VideoListModel::refresh() 0225 { 0226 if (!isLoading() && m_queryType != NoQuery) { 0227 switch (m_queryType) { 0228 case Channel: 0229 requestChannel(m_channel); 0230 break; 0231 default: 0232 requestQuery(m_queryType); 0233 } 0234 } 0235 } 0236 0237 void VideoListModel::handleQuery(QFuture<QInvidious::VideoListResult> future, QueryType type, bool reset) 0238 { 0239 // reset loaded videos (if requested) 0240 if (reset) { 0241 clearAll(); 0242 } 0243 0244 // stop running task 0245 if (m_futureWatcher) { 0246 // TODO: cancelling isn't implemented yet 0247 m_futureWatcher->cancel(); 0248 m_futureWatcher->deleteLater(); 0249 m_futureWatcher = nullptr; 0250 } 0251 0252 // set up new task 0253 m_futureWatcher = new QFutureWatcher<QInvidious::VideoListResult>(); 0254 connect(m_futureWatcher, &QFutureWatcherBase::finished, this, [this] { 0255 auto result = m_futureWatcher->result(); 0256 if (auto videos = std::get_if<QList<QInvidious::VideoBasicInfo>>(&result)) { 0257 const auto rows = rowCount(); 0258 beginInsertRows({}, rows, rows + videos->size() - 1); 0259 m_results << *videos; 0260 endInsertRows(); 0261 } else if (auto error = std::get_if<QInvidious::Error>(&result)) { 0262 Q_EMIT errorOccured(error->second); 0263 } 0264 0265 m_futureWatcher->deleteLater(); 0266 m_futureWatcher = nullptr; 0267 setLoading(false); 0268 }); 0269 0270 setQueryType(type); 0271 m_futureWatcher->setFuture(future); 0272 setLoading(true); 0273 } 0274 0275 void VideoListModel::setQueryType(QueryType type) 0276 { 0277 // title changes if the type changes or the search string has been updated (page 0) 0278 if (m_queryType != type || m_searchParameters.query().isEmpty()) { 0279 m_queryType = type; 0280 Q_EMIT titleChanged(); 0281 } 0282 } 0283 0284 void VideoListModel::clearAll() 0285 { 0286 beginResetModel(); 0287 m_results.clear(); 0288 endResetModel(); 0289 } 0290 0291 void VideoListModel::requestHistory() 0292 { 0293 setLoading(true); 0294 0295 auto pageFuture = PlasmaTube::instance().sourceManager()->selectedSource()->api()->requestHistory(m_currentPage); 0296 0297 m_historyPageWatcher = new QFutureWatcher<QInvidious::HistoryResult>(); 0298 m_historyPageWatcher->setFuture(pageFuture); 0299 0300 connect(m_historyPageWatcher, &QFutureWatcherBase::finished, this, [this] { 0301 auto result = m_historyPageWatcher->future().result(); 0302 if (auto historyList = std::get_if<QList<QString>>(&result)) { 0303 processHistoryResult(*historyList); 0304 } else if (auto error = std::get_if<QInvidious::Error>(&result)) { 0305 Q_EMIT errorOccured(error->second); 0306 } 0307 0308 m_historyPageWatcher->deleteLater(); 0309 m_historyPageWatcher = nullptr; 0310 }); 0311 0312 setQueryType(History); 0313 Q_EMIT isLoadingChanged(); 0314 } 0315 0316 void VideoListModel::processHistoryResult(const QList<QString> &result) 0317 { 0318 for (const auto &videoId : result) { 0319 auto future = PlasmaTube::instance().sourceManager()->selectedSource()->api()->requestVideo(videoId); 0320 m_historyFutureSync.addFuture(future); 0321 } 0322 0323 m_historyFetchFinishWatcher = new QFutureWatcher<void>(); 0324 m_historyFetchFinishWatcher->setFuture(QtConcurrent::run([this] { 0325 m_historyFutureSync.waitForFinished(); 0326 })); 0327 0328 connect(m_historyFetchFinishWatcher, &QFutureWatcherBase::finished, this, [this] { 0329 for (const auto &future : m_historyFutureSync.futures()) { 0330 auto result = future.result(); 0331 if (auto video = std::get_if<QInvidious::Video>(&result)) { 0332 const auto rows = rowCount(); 0333 beginInsertRows({}, rows, rows); 0334 m_results.push_back(QInvidious::VideoBasicInfo(*video)); 0335 endInsertRows(); 0336 } else if (auto error = std::get_if<QInvidious::Error>(&result)) { 0337 Q_EMIT errorOccured(error->second); 0338 } 0339 } 0340 0341 m_historyFutureSync.clearFutures(); 0342 0343 m_historyFetchFinishWatcher->deleteLater(); 0344 m_historyFetchFinishWatcher = nullptr; 0345 setLoading(false); 0346 }); 0347 } 0348 0349 void VideoListModel::markAsWatched(int row) 0350 { 0351 auto videoIndex = index(row, 0); 0352 Q_ASSERT(checkIndex(videoIndex, QAbstractItemModel::CheckIndexOption::IndexIsValid)); 0353 0354 PlasmaTube::instance().selectedSource()->markVideoWatched(data(videoIndex, IdRole).toString()); 0355 Q_EMIT dataChanged(videoIndex, videoIndex, {WatchedRole}); 0356 } 0357 0358 void VideoListModel::markAsUnwatched(int row) 0359 { 0360 auto videoIndex = index(row, 0); 0361 Q_ASSERT(checkIndex(videoIndex, QAbstractItemModel::CheckIndexOption::IndexIsValid)); 0362 0363 PlasmaTube::instance().selectedSource()->markVideoUnwatched(data(videoIndex, IdRole).toString()); 0364 Q_EMIT dataChanged(videoIndex, videoIndex, {WatchedRole}); 0365 } 0366 0367 void VideoListModel::removeFromPlaylist(const QString &plid, int index) 0368 { 0369 auto video = m_results[index]; 0370 0371 PlasmaTube::instance().sourceManager()->selectedSource()->api()->removeVideoFromPlaylist(plid, video.indexId()); 0372 0373 beginRemoveRows({}, index, index); 0374 m_results.removeAt(index); 0375 endRemoveRows(); 0376 } 0377 0378 #include "moc_videolistmodel.cpp"