From 48e407c2bd7b7c62ce747392ca2bda50d63c1fe9 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 16 Nov 2022 02:25:53 +0100 Subject: Youtube: add shorts and live streaming channel pages --- src/plugins/Info.cpp | 2 +- src/plugins/Youtube.cpp | 107 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 96 insertions(+), 13 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/Info.cpp b/src/plugins/Info.cpp index 629a08f..ed63aae 100644 --- a/src/plugins/Info.cpp +++ b/src/plugins/Info.cpp @@ -46,7 +46,7 @@ namespace QuickMedia { const std::string search_url = "https://www.google.com/search?q=" + url_param_encode(search_term); return open_with_browser(search_url); } else if(is_youtube_channel_url(args.url)) { - result_tabs.push_back(Tab{create_body(false, true), std::make_unique(program, args.url, "", "Channel videos"), create_search_bar("Search...", 350)}); + YoutubeChannelPage::create_each_type(program, args.url, "", "Channel", result_tabs); return PluginResult::OK; } else if(is_youtube_url(args.url)) { result_tabs.push_back(Tab{nullptr, std::make_unique(program, args.url, false), nullptr}); diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index d3d498e..c6e8533 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -477,6 +477,34 @@ namespace QuickMedia { return false; } + static std::optional video_item_get_reel_published_date(const Json::Value &video_item_json) { + const Json::Value &navigation_endpoint_json = video_item_json["navigationEndpoint"]; + if(!navigation_endpoint_json.isObject()) + return std::nullopt; + + const Json::Value &reel_watch_endpoint_json = navigation_endpoint_json["reelWatchEndpoint"]; + if(!reel_watch_endpoint_json.isObject()) + return std::nullopt; + + const Json::Value &overlay_json = reel_watch_endpoint_json["overlay"]; + if(!overlay_json.isObject()) + return std::nullopt; + + const Json::Value &reel_player_overlay_renderer_json = overlay_json["reelPlayerOverlayRenderer"]; + if(!reel_player_overlay_renderer_json.isObject()) + return std::nullopt; + + const Json::Value &reel_player_header_supported_renderers_json = reel_player_overlay_renderer_json["reelPlayerHeaderSupportedRenderers"]; + if(!reel_player_header_supported_renderers_json.isObject()) + return std::nullopt; + + const Json::Value &reel_player_header_renderer_json = reel_player_header_supported_renderers_json["reelPlayerHeaderRenderer"]; + if(!reel_player_header_renderer_json.isObject()) + return std::nullopt; + + return yt_json_get_text(reel_player_header_renderer_json, "timestampText"); + } + static std::shared_ptr parse_common_video_item(const Json::Value &video_item_json, std::unordered_set &added_videos) { const Json::Value &video_id_json = video_item_json["videoId"]; if(!video_id_json.isString()) @@ -487,9 +515,13 @@ namespace QuickMedia { return nullptr; std::optional title = yt_json_get_text(video_item_json, "title"); - if(!title) + std::optional headline = yt_json_get_text(video_item_json, "headline"); + if(!title && !headline) return nullptr; + if(!title) + title = std::move(headline); + std::optional date = yt_json_get_text(video_item_json, "publishedTimeText"); std::optional view_count_text = yt_json_get_text(video_item_json, "viewCountText"); std::optional owner_text = yt_json_get_text(video_item_json, "shortBylineText"); @@ -504,6 +536,9 @@ namespace QuickMedia { } } + if(!date) + date = video_item_get_reel_published_date(video_item_json); + std::string scheduled_text; const Json::Value &upcoming_event_data_json = video_item_json["upcomingEventData"]; if(upcoming_event_data_json.isObject()) { @@ -802,11 +837,14 @@ namespace QuickMedia { if(!item_content_json.isObject()) continue; - const Json::Value &video_renderer_json = item_content_json["videoRenderer"]; - if(!video_renderer_json.isObject()) - continue; + const Json::Value *video_renderer_json = &item_content_json["videoRenderer"]; + if(!video_renderer_json->isObject()) { + video_renderer_json = &item_content_json["reelItemRenderer"]; + if(!video_renderer_json->isObject()) + continue; + } - auto body_item = parse_common_video_item(video_renderer_json, added_videos); + auto body_item = parse_common_video_item(*video_renderer_json, added_videos); if(body_item) body_items.push_back(std::move(body_item)); } @@ -970,7 +1008,7 @@ namespace QuickMedia { if(strncmp(args.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(false, true), std::make_unique(program, args.url, "", args.title), create_search_bar("Search...", 350)}); + YoutubeChannelPage::create_each_type(program, args.url, "", args.title, result_tabs); } else { result_tabs.push_back(Tab{nullptr, std::make_unique(program, args.url), nullptr}); } @@ -1486,6 +1524,52 @@ namespace QuickMedia { return fetch_comments(this, video_url, continuation_token, result_items); } + static const char* youtube_channel_page_type_to_api_params(YoutubeChannelPage::Type type) { + switch(type) { + case YoutubeChannelPage::Type::VIDEOS: + return "EgZ2aWRlb3PyBgQKAjoA"; + case YoutubeChannelPage::Type::SHORTS: + return "EgZzaG9ydHPyBgUKA5oBAA%3D%3D"; + case YoutubeChannelPage::Type::LIVE: + return "EgdzdHJlYW1z8gYECgJ6AA%3D%3D"; + } + return ""; + } + + static const char* youtube_channel_page_type_get_endpoint(YoutubeChannelPage::Type type) { + switch(type) { + case YoutubeChannelPage::Type::VIDEOS: + return "videos"; + case YoutubeChannelPage::Type::SHORTS: + return "shorts"; + case YoutubeChannelPage::Type::LIVE: + return "streams"; + } + return ""; + } + + static std::string youtube_channel_page_type_to_title(const std::string &channel_name, YoutubeChannelPage::Type type) { + switch(type) { + case YoutubeChannelPage::Type::VIDEOS: + return channel_name + " Videos"; + case YoutubeChannelPage::Type::SHORTS: + return channel_name + " Shorts"; + case YoutubeChannelPage::Type::LIVE: + return channel_name + " Live videos"; + } + return ""; + } + + // static + void YoutubeChannelPage::create_each_type(Program *program, std::string url, std::string continuation_token, std::string title, std::vector &tabs) { + tabs.push_back(Tab{program->create_body(false, true), std::make_unique(program, url, continuation_token, title, Type::VIDEOS), program->create_search_bar("Search...", 350)}); + tabs.push_back(Tab{program->create_body(false, true), std::make_unique(program, url, continuation_token, title, Type::SHORTS), program->create_search_bar("Search...", 350)}); + tabs.push_back(Tab{program->create_body(false, true), std::make_unique(program, url, continuation_token, title, Type::LIVE), program->create_search_bar("Search...", 350)}); + } + + YoutubeChannelPage::YoutubeChannelPage(Program *program, std::string url, std::string continuation_token, std::string title, Type type) : + LazyFetchPage(program), TrackablePage(title, url), url(url), continuation_token(std::move(continuation_token)), title(youtube_channel_page_type_to_title(title, type)), type(type) {} + SearchResult YoutubeChannelPage::search(const std::string &str, BodyItems &result_items) { added_videos.clear(); continuation_token.clear(); @@ -1515,13 +1599,12 @@ namespace QuickMedia { client_json["clientVersion"] = "2.20210622.10.00"; client_json["osName"] = "X11"; client_json["osVersion"] = ""; - client_json["originalUrl"] = url + "/videos"; + client_json["originalUrl"] = url + "/search?query=" + url_param_encode(str); context_json["client"] = std::move(client_json); request_json["context"] = std::move(context_json); request_json["browseId"] = channel_id; request_json["query"] = str; - request_json["params"] = "EgZzZWFyY2g%3D"; - //request_json["continuation"] = current_continuation_token; + request_json["params"] = "EgZzZWFyY2jyBgQKAloA"; Json::StreamWriterBuilder json_builder; json_builder["commentStyle"] = "None"; @@ -1533,7 +1616,7 @@ namespace QuickMedia { { "-H", "content-type: application/json" }, { "-H", "x-youtube-client-name: 1" }, { "-H", youtube_client_version }, - { "-H", "referer: " + url + "/videos" }, + { "-H", "referer: " + url + "/search?query=" + url_param_encode(str) }, { "--data-raw", Json::writeString(json_builder, request_json) } }; @@ -1730,7 +1813,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&gl=US&hl=en", std::move(additional_args), true); + DownloadResult result = download_json(json_root, url + "/" + youtube_channel_page_type_get_endpoint(type) + "?pbj=1&gl=US&hl=en", std::move(additional_args), true); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); std::string browse_id; @@ -2195,7 +2278,7 @@ namespace QuickMedia { result_tabs.push_back(Tab{std::move(description_page_body), std::make_unique(program), nullptr}); result_tabs.push_back(Tab{create_body(), std::make_unique(program, url, comments_continuation_token), nullptr}); result_tabs.push_back(Tab{std::move(related_page_body), std::make_unique(program), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); - result_tabs.push_back(Tab{create_body(false, true), std::make_unique(program, channel_url, "", "Channel videos"), create_search_bar("Search...", 350)}); + YoutubeChannelPage::create_each_type(program, channel_url, "", "Channel", result_tabs); return PluginResult::OK; } -- cgit v1.2.3