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 --- plugins/Youtube.hpp | 11 ++++- src/QuickMedia.cpp | 6 +-- src/plugins/Info.cpp | 2 +- src/plugins/Youtube.cpp | 107 ++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 108 insertions(+), 18 deletions(-) diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index de8a0c3..1088df9 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -103,7 +103,15 @@ namespace QuickMedia { class YoutubeChannelPage : public LazyFetchPage, public TrackablePage { public: - YoutubeChannelPage(Program *program, std::string url, std::string continuation_token, std::string title) : LazyFetchPage(program), TrackablePage(title, url), url(url), continuation_token(std::move(continuation_token)), title(title) {} + enum class Type { + VIDEOS, + SHORTS, + LIVE + }; + + static void create_each_type(Program *program, std::string url, std::string continuation_token, std::string title, std::vector &tabs); + + YoutubeChannelPage(Program *program, std::string url, std::string continuation_token, std::string title, Type type); const char* get_title() const override { return title.c_str(); } bool search_is_filter() override { return false; } SearchResult search(const std::string &str, BodyItems &result_items) override; @@ -122,6 +130,7 @@ namespace QuickMedia { std::string continuation_token; const std::string title; int current_page = 0; + Type type; }; struct YoutubeSubscriptionTaskResult { diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index ea74c09..ae3d720 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1323,8 +1323,7 @@ namespace QuickMedia { tabs.push_back(Tab{std::move(pipe_body), std::make_unique(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "youtube") == 0) { if(!youtube_channel_url.empty()) { - auto youtube_channel_page = std::make_unique(this, youtube_channel_url, "", "Channel videos"); - tabs.push_back(Tab{create_body(false, true), std::move(youtube_channel_page), create_search_bar("Search...", 350)}); + YoutubeChannelPage::create_each_type(this, youtube_channel_url, "", "Channel", tabs); } else if(!youtube_url.empty()) { current_page = PageType::VIDEO_CONTENT; auto youtube_video_page = std::make_unique(this, youtube_url, false); @@ -6459,8 +6458,7 @@ namespace QuickMedia { std::string video_id; if(youtube_url_extract_channel_id(url, youtube_channel_id, youtube_channel_url)) { std::vector tabs; - auto youtube_channel_page = std::make_unique(this, youtube_channel_url, "", "Channel videos"); - tabs.push_back(Tab{create_body(false, true), std::move(youtube_channel_page), create_search_bar("Search...", 350)}); + YoutubeChannelPage::create_each_type(this, youtube_channel_url, "", "Channel", tabs); page_loop(tabs); redraw = true; avatar_applied = false; 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