File indexing completed on 2025-02-23 04:35:13
0001 // SPDX-FileCopyrightText: 2021 Linus Jahn <lnj@kaidan.im> 0002 // SPDX-License-Identifier: GPL-3.0-or-later 0003 0004 #include "pipedapi.h" 0005 0006 #include <KLocalizedString> 0007 0008 #include <QJsonDocument> 0009 #include <QNetworkAccessManager> 0010 #include <QNetworkReply> 0011 #include <QStringBuilder> 0012 #include <QUrl> 0013 #include <QUrlQuery> 0014 0015 const QString API_FEED = QStringLiteral("/feed"); 0016 const QString API_LOGIN = QStringLiteral("/login"); 0017 const QString API_SEARCH = QStringLiteral("/search"); 0018 const QString API_TRENDING = QStringLiteral("/trending"); 0019 const QString API_VIDEOS = QStringLiteral("/streams"); 0020 const QString API_CHANNELS = QStringLiteral("/channel"); 0021 const QString API_COMMENTS = QStringLiteral("/comments"); 0022 const QString API_PLAYLISTS = QStringLiteral("/playlists"); 0023 const QString API_CHANNEL_INFO = QStringLiteral("/channel"); 0024 0025 using namespace QInvidious; 0026 using namespace Qt::StringLiterals; 0027 0028 PipedApi::PipedApi(QNetworkAccessManager *netManager, QObject *parent) 0029 : AbstractApi(netManager, parent) 0030 { 0031 } 0032 0033 bool PipedApi::supportsFeature(AbstractApi::SupportedFeature feature) 0034 { 0035 switch (feature) { 0036 case PopularPage: 0037 case TrendingCategories: 0038 return false; 0039 } 0040 0041 return false; 0042 } 0043 0044 QFuture<LogInResult> PipedApi::logIn(QStringView username, QStringView password) 0045 { 0046 QUrlQuery params; 0047 params.addQueryItem(QStringLiteral("email"), QString::fromUtf8(QUrl::toPercentEncoding(username.toString()))); 0048 params.addQueryItem(QStringLiteral("password"), QString::fromUtf8(QUrl::toPercentEncoding(password.toString()))); 0049 params.addQueryItem(QStringLiteral("action"), QStringLiteral("signin")); 0050 0051 QUrl url = apiUrl(API_LOGIN); 0052 auto urlQuery = genericUrlQuery(); 0053 urlQuery.addQueryItem(QStringLiteral("referer"), QString::fromUtf8(QUrl::toPercentEncoding(QStringLiteral("/")))); 0054 urlQuery.addQueryItem(QStringLiteral("type"), QStringLiteral("invidious")); 0055 url.setQuery(urlQuery); 0056 0057 QNetworkRequest request(url); 0058 request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded")); 0059 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::RedirectPolicy::ManualRedirectPolicy); 0060 0061 return post<LogInResult>(std::move(request), params.toString().toUtf8(), [=](QNetworkReply *reply) -> LogInResult { 0062 const auto cookies = reply->header(QNetworkRequest::SetCookieHeader).value<QList<QNetworkCookie>>(); 0063 0064 if (!cookies.isEmpty()) { 0065 m_credentials.setUsername(username); 0066 m_credentials.setCookie(cookies.first()); 0067 Q_EMIT credentialsChanged(); 0068 return m_credentials; 0069 } 0070 return std::pair(QNetworkReply::ContentAccessDenied, i18n("Username or password is wrong.")); 0071 }); 0072 } 0073 0074 QFuture<VideoResult> PipedApi::requestVideo(QStringView videoId) 0075 { 0076 return get<VideoResult>(QNetworkRequest(videoUrl(videoId)), [=](QNetworkReply *reply) -> VideoResult { 0077 if (auto doc = QJsonDocument::fromJson(reply->readAll()); !doc.isNull()) { 0078 auto video = Video::fromJson(doc); 0079 video.setVideoId(videoId.toString()); // Piped doesn't give us the video id in the respone... 0080 return video; 0081 } 0082 return invalidJsonError(); 0083 }); 0084 } 0085 0086 QString PipedApi::resolveVideoUrl(QStringView videoId) 0087 { 0088 return QStringLiteral("ytdl://%1").arg(videoId); 0089 } 0090 0091 QFuture<SearchListResult> PipedApi::requestSearchResults(const SearchParameters ¶meters) 0092 { 0093 auto url = videoListUrl(Search, QStringLiteral(""), parameters.toPipedQueryParameters()); 0094 auto request = QNetworkRequest(url); 0095 0096 return get<SearchListResult>(std::move(request), [=](QNetworkReply *reply) -> SearchListResult { 0097 if (auto doc = QJsonDocument::fromJson(reply->readAll()); !doc.isNull()) { 0098 const auto obj = doc.object(); 0099 0100 QList<SearchResult> results; 0101 for (auto value : doc["items"_L1].toArray()) { 0102 if (value.isObject()) { 0103 results << SearchResult::fromJson(value.toObject()); 0104 } 0105 } 0106 0107 return results; 0108 } 0109 return invalidJsonError(); 0110 }); 0111 } 0112 0113 QFuture<VideoListResult> PipedApi::requestFeed(qint32 page) 0114 { 0115 QHash<QString, QString> parameters; 0116 parameters.insert(QStringLiteral("page"), QString::number(page)); 0117 0118 // TODO: piped stub 0119 return {}; 0120 } 0121 0122 QFuture<VideoListResult> PipedApi::requestTop() 0123 { 0124 return {}; 0125 } 0126 0127 QFuture<VideoListResult> PipedApi::requestTrending(TrendingTopic topic) 0128 { 0129 QHash<QString, QString> parameters; 0130 switch (topic) { 0131 case Music: 0132 parameters.insert(QStringLiteral("type"), QStringLiteral("music")); 0133 break; 0134 case Gaming: 0135 parameters.insert(QStringLiteral("type"), QStringLiteral("gaming")); 0136 break; 0137 case Movies: 0138 parameters.insert(QStringLiteral("type"), QStringLiteral("movies")); 0139 break; 0140 case News: 0141 parameters.insert(QStringLiteral("type"), QStringLiteral("news")); 0142 break; 0143 case Main: 0144 break; 0145 } 0146 return requestVideoList(Trending, QStringLiteral(""), parameters); 0147 } 0148 0149 QFuture<VideoListResult> PipedApi::requestChannel(QStringView query, qint32 page) 0150 { 0151 Q_UNUSED(page) 0152 return requestVideoList(Channel, query.toString(), {}); 0153 } 0154 0155 QFuture<SubscriptionsResult> PipedApi::requestSubscriptions() 0156 { 0157 // TODO: piped stub 0158 return {}; 0159 } 0160 0161 QFuture<Result> PipedApi::subscribeToChannel(QStringView channel) 0162 { 0163 // TODO: piped stub 0164 Q_UNUSED(channel) 0165 return {}; 0166 } 0167 0168 QFuture<Result> PipedApi::unsubscribeFromChannel(QStringView channel) 0169 { 0170 // TODO: piped stub 0171 Q_UNUSED(channel) 0172 return {}; 0173 } 0174 0175 QFuture<HistoryResult> PipedApi::requestHistory(qint32 page) 0176 { 0177 // TODO: piped stub 0178 Q_UNUSED(page) 0179 return {}; 0180 } 0181 0182 QFuture<Result> PipedApi::markWatched(const QString &videoId) 0183 { 0184 // TODO: piped stub 0185 Q_UNUSED(videoId) 0186 return {}; 0187 } 0188 0189 QFuture<Result> PipedApi::markUnwatched(const QString &videoId) 0190 { 0191 // TODO: piped stub 0192 Q_UNUSED(videoId) 0193 return {}; 0194 } 0195 0196 QFuture<CommentsResult> PipedApi::requestComments(const QString &videoId, const QString &continuation) 0197 { 0198 Q_UNUSED(continuation) 0199 0200 if (videoId.isEmpty()) { 0201 qWarning() << "Not trying to load comments for an empty video id."; 0202 return {}; 0203 } 0204 0205 QUrl url = apiUrl(API_COMMENTS % u'/' % videoId); 0206 0207 return get<CommentsResult>(authenticatedNetworkRequest(std::move(url)), [=](QNetworkReply *reply) -> CommentsResult { 0208 if (auto doc = QJsonDocument::fromJson(reply->readAll()); !doc.isNull()) { 0209 const auto array = doc[u"comments"].toArray(); 0210 0211 QList<Comment> comments; 0212 std::transform(array.cbegin(), array.cend(), std::back_inserter(comments), [](const QJsonValue &val) { 0213 Comment comment; 0214 Comment::fromJson(val.toObject(), comment); 0215 return comment; 0216 }); 0217 return Comments{comments, {}}; 0218 } 0219 return invalidJsonError(); 0220 }); 0221 } 0222 0223 QFuture<PlaylistsResult> PipedApi::requestPlaylists() 0224 { 0225 QUrl url = apiUrl(API_PLAYLISTS); 0226 0227 return get<PlaylistsResult>(authenticatedNetworkRequest(std::move(url)), [=](QNetworkReply *reply) -> PlaylistsResult { 0228 if (auto doc = QJsonDocument::fromJson(reply->readAll()); !doc.isNull()) { 0229 const auto array = doc.array(); 0230 0231 QList<Playlist> playlists; 0232 std::transform(array.cbegin(), array.cend(), std::back_inserter(playlists), [](const QJsonValue &val) { 0233 Playlist playlist; 0234 Playlist::fromJson(val.toObject(), playlist); 0235 return playlist; 0236 }); 0237 return playlists; 0238 } 0239 return invalidJsonError(); 0240 }); 0241 } 0242 0243 QFuture<PreferencesResult> PipedApi::requestPreferences() 0244 { 0245 // TODO: piped stub 0246 return {}; 0247 } 0248 0249 QFuture<Result> PipedApi::setPreferences(const QInvidious::Preferences &preferences) 0250 { 0251 // TODO: piped stub 0252 Q_UNUSED(preferences) 0253 return {}; 0254 } 0255 0256 QFuture<VideoListResult> PipedApi::requestPlaylist(const QString &plid) 0257 { 0258 QUrl url = apiUrl(API_PLAYLISTS % u'/' % plid); 0259 0260 return get<VideoListResult>(authenticatedNetworkRequest(std::move(url)), [=](QNetworkReply *reply) -> VideoListResult { 0261 if (auto doc = QJsonDocument::fromJson(reply->readAll()); !doc.isNull()) { 0262 const auto obj = doc.object(); 0263 0264 // TODO: handle pagination 0265 return VideoBasicInfo::fromJson(obj.value("relatedStreams"_L1).toArray()); 0266 } 0267 return invalidJsonError(); 0268 }); 0269 } 0270 0271 QFuture<ChannelResult> PipedApi::requestChannelInfo(QStringView queryd) 0272 { 0273 QUrl url = apiUrl(API_CHANNEL_INFO % u'/' % queryd); 0274 0275 return get<ChannelResult>(authenticatedNetworkRequest(std::move(url)), [=](QNetworkReply *reply) -> ChannelResult { 0276 if (auto doc = QJsonDocument::fromJson(reply->readAll()); !doc.isNull()) { 0277 QInvidious::Channel channel; 0278 Channel::fromJson(doc.object(), channel); 0279 return channel; 0280 } 0281 return invalidJsonError(); 0282 }); 0283 } 0284 0285 QFuture<Result> PipedApi::addVideoToPlaylist(const QString &plid, const QString &videoId) 0286 { 0287 // TODO: piped stub 0288 Q_UNUSED(plid) 0289 Q_UNUSED(videoId) 0290 return {}; 0291 } 0292 0293 QFuture<Result> PipedApi::removeVideoFromPlaylist(const QString &plid, const QString &indexId) 0294 { 0295 // TODO: piped stub 0296 Q_UNUSED(plid) 0297 Q_UNUSED(indexId) 0298 return {}; 0299 } 0300 0301 Error PipedApi::invalidJsonError() 0302 { 0303 return {QNetworkReply::InternalServerError, i18n("Server returned no valid JSON.")}; 0304 } 0305 0306 Result PipedApi::checkIsReplyOk(QNetworkReply *reply) 0307 { 0308 auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); 0309 if (status >= 200 && status < 300) { 0310 return Success(); 0311 } 0312 return std::pair(QNetworkReply::InternalServerError, i18n("Server returned the status code %1", QString::number(status))); 0313 } 0314 0315 QFuture<VideoListResult> PipedApi::requestVideoList(VideoListType queryType, const QString &urlExtension, const QHash<QString, QString> ¶meters) 0316 { 0317 auto url = videoListUrl(queryType, urlExtension, parameters); 0318 // Feed requests require to be authenticated 0319 auto request = QNetworkRequest(url); 0320 0321 return get<VideoListResult>(std::move(request), [=](QNetworkReply *reply) -> VideoListResult { 0322 if (auto doc = QJsonDocument::fromJson(reply->readAll()); !doc.isNull()) { 0323 if (queryType == Channel) { 0324 const auto obj = doc.object(); 0325 0326 auto results = VideoBasicInfo::fromJson(obj.value("relatedStreams"_L1).toArray()); 0327 return results; 0328 } else if (queryType == Search) { 0329 const auto obj = doc.object(); 0330 0331 auto results = obj.value("items"_L1).toArray(); 0332 QList<VideoBasicInfo> videoResults; 0333 for (auto value : results) { 0334 if (value.isObject() && value.toObject()["type"_L1] == "stream"_L1) { 0335 videoResults << VideoBasicInfo::fromJson(value.toObject()); 0336 } 0337 } 0338 0339 return videoResults; 0340 } else { 0341 QList<VideoBasicInfo> results; 0342 for (auto value : doc.array()) { 0343 if (value.isObject() && value.toObject()["type"_L1] == "stream"_L1) { 0344 results << VideoBasicInfo::fromJson(value.toObject()); 0345 } 0346 } 0347 0348 return results; 0349 } 0350 } 0351 return invalidJsonError(); 0352 }); 0353 } 0354 0355 QNetworkRequest PipedApi::authenticatedNetworkRequest(QUrl &&url) 0356 { 0357 QNetworkRequest request(url); 0358 if (!m_credentials.isAnonymous()) { 0359 const QList<QNetworkCookie> cookies{m_credentials.cookie().value()}; 0360 request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies)); 0361 } 0362 // some invidious instances redirect some calls using reverse proxies 0363 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); 0364 return request; 0365 } 0366 0367 QUrlQuery PipedApi::genericUrlQuery() const 0368 { 0369 QUrlQuery query; 0370 if (!m_language.isEmpty()) { 0371 query.addQueryItem(QStringLiteral("hl"), m_language); 0372 } 0373 // TODO: make configurable 0374 query.addQueryItem(QStringLiteral("region"), QStringLiteral("US")); 0375 return query; 0376 } 0377 0378 QUrl PipedApi::videoUrl(QStringView videoId) const 0379 { 0380 return apiUrl(API_VIDEOS % u'/' % videoId); 0381 } 0382 0383 QUrl PipedApi::videoListUrl(VideoListType queryType, const QString &urlExtension, const QHash<QString, QString> ¶meters) const 0384 { 0385 QString urlString; 0386 auto query = genericUrlQuery(); 0387 0388 switch (queryType) { 0389 case Search: 0390 urlString.append(API_SEARCH); 0391 break; 0392 case Trending: 0393 urlString.append(API_TRENDING); 0394 break; 0395 case Channel: 0396 urlString.append(API_CHANNELS); 0397 break; 0398 } 0399 0400 if (!urlExtension.isEmpty()) { 0401 urlString.append(QStringLiteral("/")); 0402 urlString.append(urlExtension); 0403 } 0404 0405 for (QHash<QString, QString>::const_iterator parameter = parameters.begin(); parameter != parameters.end(); ++parameter) { 0406 query.addQueryItem(parameter.key(), parameter.value()); 0407 } 0408 0409 QUrl url = apiUrl(urlString); 0410 url.setQuery(query); 0411 return url; 0412 }