From e66d24f74d5458241d869fb3df42b4f2a2ea69f4 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 6 May 2021 08:18:38 +0200 Subject: Show youtube recommendations instead of local recommendations from related videos --- src/plugins/Youtube.cpp | 250 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 232 insertions(+), 18 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 09568f6..0f5e807 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -339,12 +339,6 @@ namespace QuickMedia { return (c >= 8 && c <= 13) || c == ' '; } - static void remove_cookies_file_at_exit() { - std::lock_guard lock(cookies_mutex); - if(!cookies_filepath.empty()) - remove(cookies_filepath.c_str()); - } - // TODO: Cache this and redownload it when a network request fails with this api key? static std::string youtube_page_find_api_key() { size_t api_key_index; @@ -387,14 +381,13 @@ namespace QuickMedia { static std::vector get_cookies() { std::lock_guard lock(cookies_mutex); if(cookies_filepath.empty()) { - char filename[] = "/tmp/quickmedia.youtube.cookie.XXXXXX"; - int fd = mkstemp(filename); - if(fd == -1) - return {}; - close(fd); + Path cookies_filepath_p; + if(get_cookies_filepath(cookies_filepath_p, "youtube") != 0) { + show_notification("QuickMedia", "Failed to create youtube cookies file", Urgency::CRITICAL); + abort(); + } - cookies_filepath = filename; - atexit(remove_cookies_file_at_exit); + cookies_filepath = cookies_filepath_p.data; // TODO: Re-enable this if the api key ever changes in the future #if 0 @@ -403,10 +396,14 @@ namespace QuickMedia { api_key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"; #endif - // TODO: Is there any way to bypass this? this is needed to set VISITOR_INFO1_LIVE which is required to read comments - const char *args[] = { "curl", "-I", "-s", "-b", cookies_filepath.c_str(), "-c", cookies_filepath.c_str(), "https://www.youtube.com/subscription_manager?disable_polymer=1", nullptr }; - if(exec_program(args, nullptr, nullptr) != 0) - fprintf(stderr, "Failed to fetch cookies to view youtube comments\n"); + if(get_file_type(cookies_filepath_p) != FileType::REGULAR) { + // TODO: Is there any way to bypass this? this is needed to set VISITOR_INFO1_LIVE which is required to read comments + const char *args[] = { "curl", "-I", "-s", "-b", cookies_filepath.c_str(), "-c", cookies_filepath.c_str(), "https://www.youtube.com/subscription_manager?disable_polymer=1", nullptr }; + if(exec_program(args, nullptr, nullptr) != 0) { + show_notification("QuickMedia", "Failed to fetch cookies to view youtube comments", Urgency::CRITICAL); + abort(); + } + } } return { @@ -1451,6 +1448,191 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult YoutubeRecommendedPage::get_page(const std::string&, int page, BodyItems &result_items) { + while(current_page < page) { + PluginResult plugin_result = search_get_continuation(continuation_token, result_items); + if(plugin_result != PluginResult::OK) return plugin_result; + ++current_page; + } + return PluginResult::OK; + } + + 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::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", "referer: https://www.youtube.com/" } + }; + + 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, next_url, std::move(additional_args), true); + if(result != DownloadResult::OK) return download_result_to_plugin_result(result); + + if(!json_root.isArray()) + return PluginResult::ERR; + + std::string new_continuation_token; + for(const Json::Value &json_item : json_root) { + if(!json_item.isObject()) + continue; + + const Json::Value &response_json = json_item["response"]; + if(!response_json.isObject()) + continue; + + const Json::Value &on_response_received_actions_json = response_json["onResponseReceivedActions"]; + if(!on_response_received_actions_json.isArray()) + continue; + + for(const Json::Value &response_received_command : on_response_received_actions_json) { + if(!response_received_command.isObject()) + continue; + + const Json::Value &append_continuation_items_action_json = response_received_command["appendContinuationItemsAction"]; + if(!append_continuation_items_action_json.isObject()) + continue; + + const Json::Value &continuation_items_json = append_continuation_items_action_json["continuationItems"]; + if(!continuation_items_json.isArray()) + continue; + + for(const Json::Value &content_item_json : continuation_items_json) { + if(!content_item_json.isObject()) + continue; + + if(new_continuation_token.empty()) + new_continuation_token = item_section_renderer_get_continuation_token(content_item_json); + + const Json::Value &rich_item_renderer_json = content_item_json["richItemRenderer"]; + if(!rich_item_renderer_json.isObject()) + continue; + + const Json::Value &item_content_json = rich_item_renderer_json["content"]; + if(!item_content_json.isObject()) + continue; + + const Json::Value &video_renderer_json = item_content_json["videoRenderer"]; + if(!video_renderer_json.isObject()) + continue; + + auto body_item = parse_common_video_item(video_renderer_json, added_videos); + if(body_item) + result_items.push_back(std::move(body_item)); + } + } + } + + if(!new_continuation_token.empty()) + continuation_token = std::move(new_continuation_token); + + return PluginResult::OK; + } + + PluginResult YoutubeRecommendedPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back(Tab{nullptr, std::make_unique(program, url), nullptr}); + return PluginResult::OK; + } + + PluginResult YoutubeRecommendedPage::lazy_fetch(BodyItems &result_items) { + current_page = 0; + continuation_token.clear(); + added_videos.clear(); + + std::vector additional_args = { + { "-H", "x-youtube-client-name: 1" }, + { "-H", "x-youtube-client-version: 2.20200626.03.00" } + }; + + 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); + if(result != DownloadResult::OK) return download_result_to_plugin_result(result); + + if(!json_root.isArray()) + return PluginResult::ERR; + + std::string new_continuation_token; + for(const Json::Value &json_item : json_root) { + if(!json_item.isObject()) + continue; + + const Json::Value &response_json = json_item["response"]; + if(!response_json.isObject()) + continue; + + const Json::Value &contents_json = response_json["contents"]; + if(!contents_json.isObject()) + continue; + + const Json::Value &tcbrr_json = contents_json["twoColumnBrowseResultsRenderer"]; + if(!tcbrr_json.isObject()) + continue; + + const Json::Value &tabs_json = tcbrr_json["tabs"]; + if(!tabs_json.isArray()) + continue; + + for(const Json::Value &tab_json : tabs_json) { + if(!tab_json.isObject()) + continue; + + const Json::Value &tab_renderer_json = tab_json["tabRenderer"]; + if(!tab_renderer_json.isObject()) + continue; + + const Json::Value &content_json = tab_renderer_json["content"]; + if(!content_json.isObject()) + continue; + + const Json::Value &rich_grid_renderer_json = content_json["richGridRenderer"]; + if(!rich_grid_renderer_json.isObject()) + continue; + + const Json::Value &contents2_json = rich_grid_renderer_json["contents"]; + if(!contents2_json.isArray()) + continue; + + for(const Json::Value &content_item_json : contents2_json) { + if(!content_item_json.isObject()) + continue; + + if(new_continuation_token.empty()) + new_continuation_token = item_section_renderer_get_continuation_token(content_item_json); + + const Json::Value &rich_item_renderer_json = content_item_json["richItemRenderer"]; + if(!rich_item_renderer_json.isObject()) + continue; + + const Json::Value &item_content_json = rich_item_renderer_json["content"]; + if(!item_content_json.isObject()) + continue; + + const Json::Value &video_renderer_json = item_content_json["videoRenderer"]; + if(!video_renderer_json.isObject()) + continue; + + auto body_item = parse_common_video_item(video_renderer_json, added_videos); + if(body_item) + result_items.push_back(std::move(body_item)); + } + } + } + + if(!new_continuation_token.empty()) + continuation_token = std::move(new_continuation_token); + + return PluginResult::OK; + } + PluginResult YoutubeRelatedVideosPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { result_tabs.push_back(Tab{nullptr, std::make_unique(program, url), nullptr}); return PluginResult::OK; @@ -1512,8 +1694,8 @@ namespace QuickMedia { if(!json_item.isObject()) continue; + const Json::Value &player_response_json = json_item["playerResponse"]; if(channel_url.empty()) { - const Json::Value &player_response_json = json_item["playerResponse"]; if(player_response_json.isObject()) { const Json::Value &video_details_json = player_response_json["videoDetails"]; if(video_details_json.isObject()) { @@ -1530,6 +1712,38 @@ namespace QuickMedia { xsrf_token = xsrf_token_json.asString(); } + if(player_response_json.isObject()) { + const Json::Value &playback_tracing_json = player_response_json["playbackTracking"]; + if(playback_tracing_json.isObject()) { + if(playback_url.empty()) { + const Json::Value &video_stats_playback_url_json = playback_tracing_json["videostatsPlaybackUrl"]; + if(video_stats_playback_url_json.isObject()) { + const Json::Value &base_url_json = video_stats_playback_url_json["baseUrl"]; + if(base_url_json.isString()) + playback_url = base_url_json.asString(); + } + } + + if(watchtime_url.empty()) { + const Json::Value &video_stats_watchtime_url_json = playback_tracing_json["videostatsWatchtimeUrl"]; + if(video_stats_watchtime_url_json.isObject()) { + const Json::Value &base_url_json = video_stats_watchtime_url_json["baseUrl"]; + if(base_url_json.isString()) + watchtime_url = base_url_json.asString(); + } + } + + if(tracking_url.empty()) { + const Json::Value &p_tracking_url_json = playback_tracing_json["ptrackingUrl"]; + if(p_tracking_url_json.isObject()) { + const Json::Value &base_url_json = p_tracking_url_json["baseUrl"]; + if(base_url_json.isString()) + tracking_url = base_url_json.asString(); + } + } + } + } + const Json::Value &response_json = json_item["response"]; if(!response_json.isObject()) continue; -- cgit v1.2.3