From 4028d87367710a4cd6501314adea58678408351f Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 17 Jun 2021 03:28:09 +0200 Subject: Fix related video --- TODO | 5 +- src/plugins/Youtube.cpp | 274 ++++++++++++++++++++++++------------------------ 2 files changed, 141 insertions(+), 138 deletions(-) diff --git a/TODO b/TODO index 76bd5c5..7f3440a 100644 --- a/TODO +++ b/TODO @@ -136,7 +136,7 @@ Ctrl+F to either bring up search that searches in the body (filtering, of search The autocomplete search menu should overlay the window (with translucent black overlay) and with the search bar in the middle and autocomplete results below. Or do not have such an overlay and instead just put the autocomplete list below the search bar and close the autocomplete when pressing ESC or clicking inside the body or moving to another tab. Automatically delete old thumbnails and media files. -Ctrl+I for youtube comments that have a timestamp should have an option to jump to the timestamp in the video. +Ctrl+I for youtube comments that have a timestamp should have an option to jump to the timestamp in the video. Also do the same for the youtube video description. Ctrl+R for youtube should have a page for the video title, description, views, upload date, likes/dislikes (and maybe a button to go to the channel instead of having a channels tab?). Add a keybinding for going to the front page of the plugin. This would be especially useful for youtube and manga, where you have history, subscriptions and recommendations. Reaching end of download menu filename entry should scroll the entry vertically instead of putting the text on multiple lines. @@ -167,4 +167,5 @@ Disable drop shadow on pinephone. Load the next page in chapter list when reaching the bottom (when going to previous chapters in image view). Loading image background should be rounded. //Workaround mpv issue where video is frozen after seeking (with and without cache enabled, but more often with cache enabled). This happens because of audio. Reloading audio fixes this but audio will then be gone. -Fix youtube related videos and comments not working because of youtube update. \ No newline at end of file +Fix youtube comments not working because of youtube update. Missing xsrf_token. +Fix youtube copyrighted videos not working. Youtube triggers recaptcha for request to watch?v, but signature code is also broken? \ No newline at end of file diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 7e71830..ed30aec 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -16,6 +16,65 @@ extern "C" { #include namespace QuickMedia { + static const char *youtube_client_version = "x-youtube-client-version: 2.20210615.01.00"; + static const std::string key_api_request_data = +R"END( +{ + "context": { + "client": { + "hl": "en", + "gl": "US", + "deviceMake": "", + "deviceModel": "", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0,gzip(gfe)", + "clientName": "WEB", + "clientVersion": "2.20210615.01.00", + "osName": "Windows", + "osVersion": "10.0", + "originalUrl": "https://www.youtube.com/watch?v=%VIDEO_ID%", + "platform": "DESKTOP", + "clientFormFactor": "UNKNOWN_FORM_FACTOR", + "timeZone": "UTC", + "browserName": "Firefox", + "browserVersion": "78.0", + "screenWidthPoints": 1054, + "screenHeightPoints": 289, + "screenPixelDensity": 1, + "screenDensityFloat": 1, + "utcOffsetMinutes": 0, + "userInterfaceTheme": "USER_INTERFACE_THEME_LIGHT", + "clientScreen": "WATCH", + "mainAppWebInfo": { + "graftUrl": "/watch?v=%VIDEO_ID%", + "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER", + "isWebNativeShareAvailable": false + } + }, + "user": { + "lockedSafetyMode": false + }, + "request": { + "useSsl": true, + "internalExperimentFlags": [], + "consistencyTokenJars": [] + } + }, + "videoId": "%VIDEO_ID%", + "playbackContext": { + "contentPlaybackContext": { + "currentUrl": "/watch?v=%VIDEO_ID%", + "vis": 0, + "splay": false, + "autoCaptionsDefaultOn": false, + "autonavState": "STATE_NONE", + "html5Preference": "HTML5_PREF_WANTS" + } + }, + "racyCheckOk": false, + "contentCheckOk": false +} +)END"; + bool youtube_url_extract_id(const std::string &youtube_url, std::string &youtube_video_id) { size_t index = youtube_url.find("youtube.com/watch?v="); if(index != std::string::npos) { @@ -626,7 +685,7 @@ namespace QuickMedia { std::vector additional_args = { { "-H", "x-spf-referer: " + search_url }, { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" }, + { "-H", youtube_client_version }, { "-H", "referer: " + search_url } }; @@ -634,7 +693,7 @@ namespace QuickMedia { additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; - DownloadResult result = download_json(json_root, search_url + "&pbj=1", std::move(additional_args), true); + DownloadResult result = download_json(json_root, search_url + "&pbj=1&gl=US&hl=en", std::move(additional_args), true); if(result != DownloadResult::OK) return download_result_to_search_result(result); if(!json_root.isArray()) @@ -694,13 +753,13 @@ namespace QuickMedia { } PluginResult YoutubeSearchPage::search_get_continuation(const std::string &url, const std::string ¤t_continuation_token, BodyItems &result_items) { - std::string next_url = url + "&pbj=1&ctoken=" + current_continuation_token; + std::string next_url = url + "&pbj=1&gl=US&hl=en&ctoken=" + current_continuation_token; std::vector additional_args = { { "-H", "x-spf-referer: " + url }, { "-H", "x-youtube-client-name: 1" }, { "-H", "x-spf-previous: " + url }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" }, + { "-H", youtube_client_version }, { "-H", "referer: " + url } }; @@ -893,7 +952,7 @@ namespace QuickMedia { std::vector additional_args = { { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-youtube-client-version: 2.20210308.08.00" }, + { "-H", youtube_client_version }, { "-F", "session_token=" + xsrf_token } }; @@ -975,7 +1034,7 @@ namespace QuickMedia { std::vector additional_args = { { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-youtube-client-version: 2.20210308.08.00" }, + { "-H", youtube_client_version }, { "-F", "session_token=" + xsrf_token } }; @@ -1081,7 +1140,7 @@ namespace QuickMedia { { "-H", "x-origin: https://www.youtube.com" }, { "-H", "content-type: application/json" }, { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" }, + { "-H", youtube_client_version }, { "-H", "referer: " + url + "/videos" }, { "--data-raw", Json::writeString(json_builder, request_json) } }; @@ -1164,7 +1223,7 @@ namespace QuickMedia { { "-H", "x-origin: https://www.youtube.com" }, { "-H", "content-type: application/json" }, { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" }, + { "-H", youtube_client_version }, { "-H", "referer: " + url + "/videos" }, { "--data-raw", Json::writeString(json_builder, request_json) } }; @@ -1232,7 +1291,7 @@ namespace QuickMedia { { "-H", "x-spf-referer: " + url }, { "-H", "x-youtube-client-name: 1" }, { "-H", "x-spf-previous: " + url }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" }, + { "-H", youtube_client_version }, { "-H", "referer: " + url } }; @@ -1240,7 +1299,7 @@ namespace QuickMedia { additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; - DownloadResult result = download_json(json_root, url + "/videos?pbj=1", std::move(additional_args), true); + DownloadResult result = download_json(json_root, url + "/videos?pbj=1&gl=US&hl=en", std::move(additional_args), true); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); result_items = parse_channel_videos(json_root, continuation_token, added_videos); return PluginResult::OK; @@ -1486,13 +1545,13 @@ namespace QuickMedia { } PluginResult YoutubeRecommendedPage::search_get_continuation(const std::string ¤t_continuation_token, BodyItems &result_items) { - std::string next_url = "https://www.youtube.com/?pbj=1&ctoken=" + current_continuation_token; + std::string next_url = "https://www.youtube.com/?pbj=1&gl=US&hl=en&ctoken=" + current_continuation_token; std::vector additional_args = { { "-H", "x-spf-referer: https://www.youtube.com/" }, { "-H", "x-youtube-client-name: 1" }, { "-H", "x-spf-previous: https://www.youtube.com/" }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" }, + { "-H", youtube_client_version }, { "-H", "referer: https://www.youtube.com/" } }; @@ -1575,14 +1634,14 @@ namespace QuickMedia { std::vector additional_args = { { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" } + { "-H", youtube_client_version } }; std::vector cookies = get_cookies(); additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; - DownloadResult result = download_json(json_root, "https://www.youtube.com/?pbj=1", std::move(additional_args), true); + DownloadResult result = download_json(json_root, "https://www.youtube.com/?pbj=1&gl=US&hl=en", std::move(additional_args), true); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); if(!json_root.isArray()) @@ -1695,90 +1754,84 @@ namespace QuickMedia { BodyItems YoutubeVideoPage::get_related_media(const std::string &url) { BodyItems result_items; - std::string modified_url = remove_index_from_playlist_url(url); + std::string video_id; + if(!youtube_url_extract_id(url, video_id)) { + fprintf(stderr, "Failed to extract youtube id from %s\n", url.c_str()); + return result_items; + } + + std::string request_data = key_api_request_data; + string_replace_all(request_data, "%VIDEO_ID%", video_id); + std::vector additional_args = { - { "-H", "x-spf-referer: " + url }, + { "-H", "Content-Type: application/json" }, { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-spf-previous: " + url }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" }, - { "-H", "referer: " + url } + { "-H", youtube_client_version }, + { "--data-raw", std::move(request_data) } }; std::vector cookies = get_cookies(); additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; - DownloadResult result = download_json(json_root, modified_url + "&pbj=1", std::move(additional_args), true); - if(result != DownloadResult::OK) return result_items; + DownloadResult download_result = download_json(json_root, "https://www.youtube.com/youtubei/v1/next?key=" + api_key, additional_args, true); + if(download_result != DownloadResult::OK) return result_items; - if(!json_root.isArray()) + if(!json_root.isObject()) return result_items; std::unordered_set added_videos; xsrf_token.clear(); comments_continuation_token.clear(); - for(const Json::Value &json_item : json_root) { - if(!json_item.isObject()) - continue; + // TODO: Find xsrf_token somehow. Maybe use /embed/ endpoint to find it? xsrf_token is needed for comments - if(xsrf_token.empty()) { - const Json::Value &xsrf_token_json = json_item["xsrf_token"]; - if(xsrf_token_json.isString()) - xsrf_token = xsrf_token_json.asString(); - } + const Json::Value &contents_json = json_root["contents"]; + if(!contents_json.isObject()) + return result_items; - const Json::Value &response_json = json_item["response"]; - if(!response_json.isObject()) - continue; + const Json::Value &tcwnr_json = contents_json["twoColumnWatchNextResults"]; + if(!tcwnr_json.isObject()) + return result_items; - const Json::Value &contents_json = response_json["contents"]; - if(!contents_json.isObject()) - return result_items; + if(comments_continuation_token.empty()) + comments_continuation_token = two_column_watch_next_results_get_comments_continuation_token(tcwnr_json); - const Json::Value &tcwnr_json = contents_json["twoColumnWatchNextResults"]; - if(!tcwnr_json.isObject()) - return result_items; + const Json::Value &secondary_results_json = tcwnr_json["secondaryResults"]; + if(!secondary_results_json.isObject()) + return result_items; - if(comments_continuation_token.empty()) - comments_continuation_token = two_column_watch_next_results_get_comments_continuation_token(tcwnr_json); + const Json::Value &secondary_results2_json = secondary_results_json["secondaryResults"]; + if(!secondary_results2_json.isObject()) + return result_items; + + const Json::Value &results_json = secondary_results2_json["results"]; + if(!results_json.isArray()) + return result_items; - const Json::Value &secondary_results_json = tcwnr_json["secondaryResults"]; - if(!secondary_results_json.isObject()) - return result_items; + for(const Json::Value &item_json : results_json) { + if(!item_json.isObject()) + continue; - const Json::Value &secondary_results2_json = secondary_results_json["secondaryResults"]; - if(!secondary_results2_json.isObject()) - return result_items; + auto body_item = parse_compact_video_renderer_json(item_json, added_videos); + if(body_item) + result_items.push_back(std::move(body_item)); - const Json::Value &results_json = secondary_results2_json["results"]; - if(!results_json.isArray()) - return result_items; - - for(const Json::Value &item_json : results_json) { - if(!item_json.isObject()) + const Json::Value &compact_autoplay_renderer_json = item_json["compactAutoplayRenderer"]; + if(!compact_autoplay_renderer_json.isObject()) + continue; + + const Json::Value &item_contents_json = compact_autoplay_renderer_json["contents"]; + if(!item_contents_json.isArray()) + continue; + + for(const Json::Value &content_item_json : item_contents_json) { + if(!content_item_json.isObject()) continue; - - auto body_item = parse_compact_video_renderer_json(item_json, added_videos); + + auto body_item = parse_compact_video_renderer_json(content_item_json, added_videos); if(body_item) result_items.push_back(std::move(body_item)); - - const Json::Value &compact_autoplay_renderer_json = item_json["compactAutoplayRenderer"]; - if(!compact_autoplay_renderer_json.isObject()) - continue; - - const Json::Value &item_contents_json = compact_autoplay_renderer_json["contents"]; - if(!item_contents_json.isArray()) - continue; - - for(const Json::Value &content_item_json : item_contents_json) { - if(!content_item_json.isObject()) - continue; - - auto body_item = parse_compact_video_renderer_json(content_item_json, added_videos); - if(body_item) - result_items.push_back(std::move(body_item)); - } } } @@ -1903,76 +1956,20 @@ namespace QuickMedia { return PluginResult::ERR; } + std::string request_data = key_api_request_data; + string_replace_all(request_data, "%VIDEO_ID%", video_id); + std::vector additional_args = { + { "-H", "Content-Type: application/json" }, { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" } + { "-H", youtube_client_version }, + { "--data-raw", std::move(request_data) } }; std::vector cookies = get_cookies(); additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; - std::string request_data = -R"END( -{ - "context": { - "client": { - "hl": "en", - "gl": "US", - "deviceMake": "", - "deviceModel": "", - "userAgent": "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0,gzip(gfe)", - "clientName": "WEB", - "clientVersion": "2.20210615.01.00", - "osName": "Windows", - "osVersion": "10.0", - "originalUrl": "https://www.youtube.com/watch?v=%VIDEO_ID%", - "platform": "DESKTOP", - "clientFormFactor": "UNKNOWN_FORM_FACTOR", - "timeZone": "UTC", - "browserName": "Firefox", - "browserVersion": "78.0", - "screenWidthPoints": 1054, - "screenHeightPoints": 289, - "screenPixelDensity": 1, - "screenDensityFloat": 1, - "utcOffsetMinutes": 0, - "userInterfaceTheme": "USER_INTERFACE_THEME_LIGHT", - "clientScreen": "WATCH", - "mainAppWebInfo": { - "graftUrl": "/watch?v=%VIDEO_ID%", - "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER", - "isWebNativeShareAvailable": false - } - }, - "user": { - "lockedSafetyMode": false - }, - "request": { - "useSsl": true, - "internalExperimentFlags": [], - "consistencyTokenJars": [] - } - }, - "videoId": "%VIDEO_ID%", - "playbackContext": { - "contentPlaybackContext": { - "currentUrl": "/watch?v=%VIDEO_ID%", - "vis": 0, - "splay": false, - "autoCaptionsDefaultOn": false, - "autonavState": "STATE_NONE", - "html5Preference": "HTML5_PREF_WANTS" - } - }, - "racyCheckOk": false, - "contentCheckOk": false -} -)END"; - string_replace_all(request_data, "%VIDEO_ID%", video_id); - additional_args.push_back({ "-H", "Content-Type: application/json" }); - additional_args.push_back({ "--data-raw", std::move(request_data) }); - DownloadResult download_result = download_json(json_root, "https://www.youtube.com/youtubei/v1/player?key=" + api_key, additional_args, true); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); @@ -2076,7 +2073,7 @@ R"END( std::vector additional_args = { { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" } + { "-H", youtube_client_version } }; std::vector cookies = get_cookies(); @@ -2109,7 +2106,12 @@ R"END( if(cipher_params.empty() || url.empty()) return false; + std::string cpn; + cpn.resize(16); + generate_random_characters(cpn.data(), cpn.size(), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", 64); + std::string url_decoded = url_param_decode(url); + url_decoded += "&alr=yes&cver=2.20210615.01.00&altitags=395,394&cpn=" + cpn; const std::string &s = cipher_params["s"]; const std::string &sp = cipher_params["sp"]; @@ -2210,6 +2212,6 @@ R"END( parse_format(formats_json, false); const Json::Value &adaptive_formats_json = streaming_data_json["adaptiveFormats"]; - parse_format(adaptive_formats_json, false); + parse_format(adaptive_formats_json, true); } } \ No newline at end of file -- cgit v1.2.3