From f8617f2043ea4ec536c4622df63a77b25268aeb0 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 6 Mar 2021 14:25:48 +0100 Subject: Youtube: fix channel next page, add proper channel search (non filter search) --- src/QuickMedia.cpp | 94 +++++++++--------- src/plugins/Plugin.cpp | 1 + src/plugins/Youtube.cpp | 253 +++++++++++++++++++++++++++++++++--------------- 3 files changed, 222 insertions(+), 126 deletions(-) (limited to 'src') diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 6b480d2..03fe6b2 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -72,62 +72,53 @@ static const XRRModeInfo* get_mode_info(const XRRScreenResources *sr, RRMode id) return nullptr; } -static int get_monitor_max_hz(Display *display) { +static void for_each_active_monitor_output(Display *display, std::function callback_func) { XRRScreenResources *screen_res = XRRGetScreenResources(display, DefaultRootWindow(display)); - if(screen_res) { - unsigned long max_hz = 0; - for(int i = 0; i < screen_res->noutput; ++i) { - XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]); - if(out_info && out_info->connection == RR_Connected) { - XRRCrtcInfo *crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc); - if(crt_info) { - const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode); - if(mode_info) { - unsigned long total = mode_info->hTotal * mode_info->vTotal; - if(total > 0) - max_hz = std::max(max_hz, mode_info->dotClock / total); - } - XRRFreeCrtcInfo(crt_info); - } + if(!screen_res) + return; + + for(int i = 0; i < screen_res->noutput; ++i) { + XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]); + if(out_info && out_info->connection == RR_Connected) { + XRRCrtcInfo *crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc); + if(crt_info) { + const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode); + if(mode_info) + callback_func(mode_info); + XRRFreeCrtcInfo(crt_info); } - if(out_info) - XRRFreeOutputInfo(out_info); } - XRRFreeScreenResources(screen_res); - if(max_hz == 0) - max_hz = 60; - return std::min(max_hz, 144UL); + if(out_info) + XRRFreeOutputInfo(out_info); } - return 60; + + XRRFreeScreenResources(screen_res); +} + +static int get_monitor_max_hz(Display *display) { + unsigned long max_hz = 0; + for_each_active_monitor_output(display, [&max_hz](const XRRModeInfo *mode_info) { + unsigned long total = mode_info->hTotal * mode_info->vTotal; + if(total > 0) + max_hz = std::max(max_hz, (unsigned long)std::round((double)mode_info->dotClock / (double)total)); + }); + + if(max_hz == 0) + return 60; + return std::min(max_hz, 144UL); } static int get_largest_monitor_height(Display *display) { - XRRScreenResources *screen_res = XRRGetScreenResources(display, DefaultRootWindow(display)); - if(screen_res) { - int max_height = 0; - for(int i = 0; i < screen_res->noutput; ++i) { - XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]); - if(out_info && out_info->connection == RR_Connected) { - XRRCrtcInfo* crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc); - if(crt_info) { - const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode); - if(mode_info) { - // Need to get the min of width or height because we want to get the smallest size for monitors in portrait mode, for mobile devices such as pinephone - int width_or_height = std::min((int)mode_info->width, (int)mode_info->height); - max_height = std::max(max_height, width_or_height); - } - XRRFreeCrtcInfo(crt_info); - } - } - if(out_info) - XRRFreeOutputInfo(out_info); - } - XRRFreeScreenResources(screen_res); - if(max_height == 0) - max_height = 720; - return std::max(max_height, 480); - } - return 720; + int max_height = 0; + for_each_active_monitor_output(display, [&max_height](const XRRModeInfo *mode_info) { + // Need to get the min of width or height because we want to get the smallest size for monitors in portrait mode, for mobile devices such as pinephone + int width_or_height = std::min((int)mode_info->width, (int)mode_info->height); + max_height = std::max(max_height, width_or_height); + }); + + if(max_height == 0) + return 720; + return std::max(max_height, 480); } static void get_screen_resolution(Display *display, int *width, int *height) { @@ -1903,7 +1894,7 @@ namespace QuickMedia { tabs.push_back(Tab{std::move(related_videos_body), std::move(related_videos_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } if(channels_page) { - tabs.push_back(Tab{create_body(), std::move(channels_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + tabs.push_back(Tab{create_body(), std::move(channels_page), create_search_bar("Search...", is_youtube ? 350 : SEARCH_DELAY_FILTER)}); } bool page_changed = false; @@ -2518,6 +2509,9 @@ namespace QuickMedia { images_to_upscale_queue.clear(); image_upscale_status.clear(); } + + window_size.x = window.getSize().x; + window_size.y = window.getSize().y; } static bool is_url_video(const std::string &url) { diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp index a391d06..33fe47e 100644 --- a/src/plugins/Plugin.cpp +++ b/src/plugins/Plugin.cpp @@ -6,4 +6,5 @@ namespace QuickMedia { SearchResult download_result_to_search_result(DownloadResult download_result) { return (SearchResult)download_result; } ImageResult download_result_to_image_result(DownloadResult download_result) { return (ImageResult)download_result; } PluginResult search_result_to_plugin_result(SearchResult search_result) { return (PluginResult)search_result; } + SearchResult plugin_result_to_search_result(PluginResult plugin_result) { return (SearchResult)plugin_result; } } \ No newline at end of file diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index cc20f2f..b3c9a60 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -3,6 +3,7 @@ #include "../../include/NetUtils.hpp" #include "../../include/StringUtils.hpp" #include "../../include/Scale.hpp" +#include #include namespace QuickMedia { @@ -250,28 +251,10 @@ namespace QuickMedia { return token_json.asString(); } - static std::string grid_renderer_get_continuation_token(const Json::Value &grid_renderer_json) { - const Json::Value &continuations_json = grid_renderer_json["continuations"]; - if(!continuations_json.isArray()) - return ""; - - for(const Json::Value &continuation_json : continuations_json) { - if(!continuation_json.isObject()) - continue; - - const Json::Value &next_continuation_data_json = continuation_json["nextContinuationData"]; - if(!next_continuation_data_json.isObject()) - continue; - - const Json::Value &continuation_item_json = next_continuation_data_json["continuation"]; - if(continuation_item_json.isString()) - return continuation_item_json.asString(); - } - - return ""; - } - static void parse_item_section_renderer(const Json::Value &item_section_renderer_json, std::unordered_set &added_videos, BodyItems &result_items) { + if(!item_section_renderer_json.isObject()) + return; + const Json::Value &item_contents_json = item_section_renderer_json["contents"]; if(!item_contents_json.isArray()) return; @@ -399,9 +382,6 @@ namespace QuickMedia { if(!grid_renderer_json.isObject()) continue; - if(new_continuation_token.empty()) - new_continuation_token = grid_renderer_get_continuation_token(grid_renderer_json); - const Json::Value &items_json = grid_renderer_json["items"]; if(!items_json.isArray()) continue; @@ -410,6 +390,9 @@ namespace QuickMedia { if(!item_json.isObject()) continue; + if(new_continuation_token.empty()) + new_continuation_token = item_section_renderer_get_continuation_token(item_json); + const Json::Value &grid_video_renderer = item_json["gridVideoRenderer"]; if(!grid_video_renderer.isObject()) continue; @@ -429,6 +412,29 @@ namespace QuickMedia { return body_items; } + static void parse_section_list_renderer(const Json::Value §ion_list_renderer_json, std::string &continuation_token, BodyItems &result_items, std::unordered_set &added_videos) { + if(!section_list_renderer_json.isObject()) + return; + + const Json::Value &contents2_json = section_list_renderer_json["contents"]; + if(!contents2_json.isArray()) + return; + + for(const Json::Value &item_json : contents2_json) { + if(!item_json.isObject()) + continue; + + if(continuation_token.empty()) + continuation_token = item_section_renderer_get_continuation_token(item_json); + + const Json::Value &item_section_renderer_json = item_json["itemSectionRenderer"]; + if(!item_section_renderer_json.isObject()) + continue; + + parse_item_section_renderer(item_section_renderer_json, added_videos, result_items); + } + } + SearchResult YoutubeSearchPage::search(const std::string &str, BodyItems &result_items) { continuation_token.clear(); current_page = 0; @@ -474,27 +480,7 @@ namespace QuickMedia { if(!primary_contents_json.isObject()) continue; - const Json::Value §ion_list_renderer_json = primary_contents_json["sectionListRenderer"]; - if(!section_list_renderer_json.isObject()) - continue; - - const Json::Value &contents2_json = section_list_renderer_json["contents"]; - if(!contents2_json.isArray()) - continue; - - for(const Json::Value &item_json : contents2_json) { - if(!item_json.isObject()) - continue; - - if(continuation_token.empty()) - continuation_token = item_section_renderer_get_continuation_token(item_json); - - const Json::Value &item_section_renderer_json = item_json["itemSectionRenderer"]; - if(!item_section_renderer_json.isObject()) - continue; - - parse_item_section_renderer(item_section_renderer_json, added_videos, result_items); - } + parse_section_list_renderer(primary_contents_json["sectionListRenderer"], continuation_token, result_items, added_videos); } return SearchResult::OK; @@ -515,7 +501,7 @@ namespace QuickMedia { if(strncmp(url.c_str(), "https://www.youtube.com/channel/", 32) == 0) { // TODO: Make all pages (for all services) lazy fetch in a similar manner! - result_tabs.push_back(Tab{create_body(), std::make_unique(program, url, "", title), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + result_tabs.push_back(Tab{create_body(), std::make_unique(program, url, "", title), create_search_bar("Search...", 350)}); } else { result_tabs.push_back(Tab{nullptr, std::make_unique(program), nullptr}); } @@ -592,6 +578,102 @@ namespace QuickMedia { return PluginResult::OK; } + static std::string channel_url_extract_id(const std::string &channel_url) { + size_t index = channel_url.find("channel/"); + if(index == std::string::npos) + return ""; + + index += 8; + size_t end_index = channel_url.find('/', index); + if(end_index == std::string::npos) + return channel_url.substr(index); + + return channel_url.substr(index, end_index - index); + } + + SearchResult YoutubeChannelPage::search(const std::string &str, BodyItems &result_items) { + added_videos.clear(); + continuation_token.clear(); + current_page = 0; + if(str.empty()) + return plugin_result_to_search_result(lazy_fetch(result_items)); + + std::string next_url = "https://www.youtube.com/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"; + + Json::Value request_json(Json::objectValue); + Json::Value context_json(Json::objectValue); + Json::Value client_json(Json::objectValue); + client_json["hl"] = "en"; + client_json["gl"] = "US"; + client_json["deviceMake"] = ""; + client_json["deviceModel"] = ""; + client_json["userAgent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"; + client_json["clientName"] = "WEB"; + client_json["clientVersion"] = "2.20210304.08.01"; + client_json["osName"] = "X11"; + client_json["osVersion"] = ""; + client_json["originalUrl"] = url + "/videos"; + context_json["client"] = std::move(client_json); + request_json["context"] = std::move(context_json); + request_json["browseId"] = channel_url_extract_id(url); + request_json["query"] = str; + request_json["params"] = "EgZzZWFyY2g%3D"; + //request_json["continuation"] = current_continuation_token; + + Json::StreamWriterBuilder json_builder; + json_builder["commentStyle"] = "None"; + json_builder["indentation"] = ""; + + std::vector additional_args = { + { "-H", "authority: www.youtube.com" }, + { "-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", "referer: " + url + "/videos" }, + { "--data-raw", Json::writeString(json_builder, request_json) } + }; + + //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_search_result(result); + + if(!json_root.isObject()) + return SearchResult::ERR; + + const Json::Value &contents_json = json_root["contents"]; + if(!contents_json.isObject()) + return SearchResult::ERR; + + const Json::Value &two_column_browse_results_renderer_json = contents_json["twoColumnBrowseResultsRenderer"]; + if(!two_column_browse_results_renderer_json.isObject()) + return SearchResult::ERR; + + const Json::Value &tabs_json = two_column_browse_results_renderer_json["tabs"]; + if(!tabs_json.isArray()) + return SearchResult::ERR; + + for(const Json::Value &json_item : tabs_json) { + if(!json_item.isObject()) + continue; + + const Json::Value &expandable_tab_renderer_json = json_item["expandableTabRenderer"]; + if(!expandable_tab_renderer_json.isObject()) + continue; + + const Json::Value &content_json = expandable_tab_renderer_json["content"]; + if(!content_json.isObject()) + continue; + + parse_section_list_renderer(content_json["sectionListRenderer"], continuation_token, result_items, added_videos); + } + + return SearchResult::OK; + } + PluginResult YoutubeChannelPage::get_page(const std::string&, int page, BodyItems &result_items) { while(current_page < page) { PluginResult plugin_result = search_get_continuation(url, continuation_token, result_items); @@ -602,14 +684,37 @@ namespace QuickMedia { } PluginResult YoutubeChannelPage::search_get_continuation(const std::string &url, const std::string ¤t_continuation_token, BodyItems &result_items) { - std::string next_url = "https://www.youtube.com/browse_ajax?ctoken=" + current_continuation_token + "&continuation=" + current_continuation_token; + std::string next_url = "https://www.youtube.com/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"; + + Json::Value request_json(Json::objectValue); + Json::Value context_json(Json::objectValue); + Json::Value client_json(Json::objectValue); + client_json["hl"] = "en"; + client_json["gl"] = "US"; + client_json["deviceMake"] = ""; + client_json["deviceModel"] = ""; + client_json["userAgent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"; + client_json["clientName"] = "WEB"; + client_json["clientVersion"] = "2.20210304.08.01"; + client_json["osName"] = "X11"; + client_json["osVersion"] = ""; + client_json["originalUrl"] = url + "/videos"; + context_json["client"] = std::move(client_json); + request_json["context"] = std::move(context_json); + request_json["continuation"] = current_continuation_token; + + Json::StreamWriterBuilder json_builder; + json_builder["commentStyle"] = "None"; + json_builder["indentation"] = ""; std::vector additional_args = { - { "-H", "x-spf-referer: " + url }, + { "-H", "authority: www.youtube.com" }, + { "-H", "x-origin: https://www.youtube.com" }, + { "-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", "referer: " + url + "/videos" }, + { "--data-raw", Json::writeString(json_builder, request_json) } }; //std::vector cookies = get_cookies(); @@ -619,46 +724,41 @@ namespace QuickMedia { 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()) + if(!json_root.isObject()) + return PluginResult::ERR; + + const Json::Value &on_response_received_actions_json = json_root["onResponseReceivedActions"]; + if(!on_response_received_actions_json.isArray()) return PluginResult::ERR; std::string new_continuation_token; - for(const Json::Value &json_item : json_root) { + for(const Json::Value &json_item : on_response_received_actions_json) { if(!json_item.isObject()) continue; - const Json::Value &response_json = json_item["response"]; - if(!response_json.isObject()) - continue; - - const Json::Value &continuation_contents_json = response_json["continuationContents"]; - if(!continuation_contents_json.isObject()) - continue; - - const Json::Value &grid_continuation_json = continuation_contents_json["gridContinuation"]; - if(!grid_continuation_json.isObject()) + const Json::Value &append_continuation_items_action_json = json_item["appendContinuationItemsAction"]; + if(!append_continuation_items_action_json.isObject()) continue; - if(new_continuation_token.empty()) { - // grid_continuation_json is compatible with grid_renderer - new_continuation_token = grid_renderer_get_continuation_token(grid_continuation_json); - } - - const Json::Value &items_json = grid_continuation_json["items"]; - if(!items_json.isArray()) + const Json::Value &continuation_items_json = append_continuation_items_action_json["continuationItems"]; + if(!continuation_items_json.isArray()) continue; - for(const Json::Value &item_json : items_json) { + for(const Json::Value &item_json : continuation_items_json) { if(!item_json.isObject()) continue; - const Json::Value &grid_video_renderer = item_json["gridVideoRenderer"]; - if(!grid_video_renderer.isObject()) - continue; + if(new_continuation_token.empty()) + new_continuation_token = item_section_renderer_get_continuation_token(item_json); - auto body_item = parse_common_video_item(grid_video_renderer, added_videos); - if(body_item) - result_items.push_back(std::move(body_item)); + const Json::Value &grid_video_renderer = item_json["gridVideoRenderer"]; + if(grid_video_renderer.isObject()) { + auto body_item = parse_common_video_item(grid_video_renderer, added_videos); + if(body_item) + result_items.push_back(std::move(body_item)); + } else { + parse_item_section_renderer(item_json["itemSectionRenderer"], added_videos, result_items); + } } } @@ -676,6 +776,7 @@ namespace QuickMedia { } PluginResult YoutubeChannelPage::lazy_fetch(BodyItems &result_items) { + added_videos.clear(); std::vector additional_args = { { "-H", "x-spf-referer: " + url }, { "-H", "x-youtube-client-name: 1" }, -- cgit v1.2.3