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"