From 89e018c4f24a03a9436024539d10d2b058d6e956 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 29 Oct 2023 14:22:24 +0100 Subject: Youtube: support playlists --- src/QuickMedia.cpp | 19 ++-- src/plugins/Youtube.cpp | 295 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 258 insertions(+), 56 deletions(-) (limited to 'src') diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 513290e..2083df3 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -3471,15 +3471,20 @@ namespace QuickMedia { current_page = previous_page; go_to_previous_page = true; } else { - if(video_page->autoplay_next_item()) - return; + const bool autoplay_next_item = video_page->autoplay_next_item(); std::string url = video_page->get_url(); - related_videos.clear(); - related_videos_task = AsyncTask([&related_videos, url, video_page]() { - video_page->mark_watched(); - related_videos = video_page->get_related_media(url); - }); + if(autoplay_next_item) { + related_videos_task = AsyncTask([url, video_page]() { + video_page->get_related_media(url); + }); + } else { + related_videos.clear(); + related_videos_task = AsyncTask([&related_videos, url, video_page]() { + video_page->mark_watched(); + related_videos = video_page->get_related_media(url); + }); + } // TODO: Make this also work for other video plugins if(strcmp(plugin_name, "youtube") != 0 || is_resume_go_back) diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 2959223..2e59a0b 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -538,6 +538,9 @@ namespace QuickMedia { } static std::shared_ptr parse_common_video_item(const Json::Value &video_item_json, std::unordered_set &added_videos) { + if(!video_item_json.isObject()) + return nullptr; + const Json::Value &video_id_json = video_item_json["videoId"]; if(!video_id_json.isString()) return nullptr; @@ -640,17 +643,6 @@ namespace QuickMedia { return body_item; } - static std::shared_ptr parse_content_video_renderer(const Json::Value &content_item_json, std::unordered_set &added_videos) { - if(!content_item_json.isObject()) - return nullptr; - - const Json::Value &video_renderer_json = content_item_json["videoRenderer"]; - if(!video_renderer_json.isObject()) - return nullptr; - - return parse_common_video_item(video_renderer_json, added_videos); - } - static std::shared_ptr parse_channel_renderer(const Json::Value &channel_renderer_json) { if(!channel_renderer_json.isObject()) return nullptr; @@ -724,6 +716,139 @@ namespace QuickMedia { return token_json.asString(); } + static std::shared_ptr parse_child_video_renderer(const Json::Value &child_video_renderer_json) { + if(!child_video_renderer_json.isObject()) + return nullptr; + + const Json::Value &video_id_json = child_video_renderer_json["videoId"]; + if(!video_id_json.isString()) + return nullptr; + + std::optional title = yt_json_get_text(child_video_renderer_json, "title"); + std::optional length = yt_json_get_text(child_video_renderer_json, "lengthText"); + + std::string title_str = title.value_or("No title") + " • " + length.value_or("0:00"); + auto body_item = BodyItem::create(std::move(title_str)); + body_item->url = video_id_json.asString(); + return body_item; + } + + static bool navigation_endpoint_get_video_id(const Json::Value &navigation_endpoint_json, std::string &video_id) { + if(!navigation_endpoint_json.isObject()) + return false; + + const Json::Value &watch_endpoint_json = navigation_endpoint_json["watchEndpoint"]; + if(!watch_endpoint_json.isObject()) + return false; + + const Json::Value &video_id_json = watch_endpoint_json["videoId"]; + if(!video_id_json.isString()) + return false; + + video_id = video_id_json.asString(); + return true; + } + + static std::shared_ptr parse_playlist_renderer(const Json::Value &playlist_renderer_json) { + if(!playlist_renderer_json.isObject()) + return nullptr; + + const Json::Value &playlist_id_json = playlist_renderer_json["playlistId"]; + const Json::Value &videos_json = playlist_renderer_json["videos"]; + const Json::Value &video_count_json = playlist_renderer_json["videoCount"]; + if(!playlist_id_json.isString()) + return nullptr; + + std::optional video_count_short_text = yt_json_get_text(playlist_renderer_json, "videoCountShortText"); + std::string video_count_text; + if(video_count_short_text) + video_count_text = video_count_short_text.value(); + else if(video_count_json.isString()) + video_count_text = video_count_json.asString(); + + std::optional title = yt_json_get_text(playlist_renderer_json, "title"); + + std::optional thumbnail; + const Json::Value &thumbnail_json = playlist_renderer_json["thumbnail"]; + if(thumbnail_json.isObject()) + thumbnail = yt_json_get_thumbnail(thumbnail_json, ThumbnailSize::LARGEST); + + const Json::Value &thumbnails_json = playlist_renderer_json["thumbnails"]; + if(thumbnails_json.isArray() && !thumbnails_json.empty()) + thumbnail = yt_json_get_thumbnail(thumbnails_json[0], ThumbnailSize::LARGEST); + + std::string long_byline_text = yt_json_get_text(playlist_renderer_json, "longBylineText").value_or(""); + if(long_byline_text.empty()) + long_byline_text = yt_json_get_text(playlist_renderer_json, "shortBylineText").value_or(""); + + std::string video_id; + std::string description = std::move(long_byline_text); + if(!description.empty()) + description += '\n'; + description += video_count_text + " video" + (strcmp(video_count_text.c_str(), "1") == 0 ? "" : "s"); + if(videos_json.isArray()) { + for(const Json::Value &video_json : videos_json) { + if(!video_json.isObject()) + continue; + + auto video_body_item = parse_child_video_renderer(video_json["childVideoRenderer"]); + if(video_body_item) { + //description += '\n'; + //description += video_body_item->get_title(); + if(video_id.empty()) + video_id = video_body_item->url; + } + } + } + + if(video_id.empty()) + navigation_endpoint_get_video_id(playlist_renderer_json["navigationEndpoint"], video_id); + + if(video_id.empty()) + return nullptr; + + auto body_item = BodyItem::create(title.value_or("No title")); + body_item->set_description(std::move(description)); + body_item->set_description_color(get_theme().faded_text_color); + body_item->url = "https://www.youtube.com/watch?v=" + video_id + "&list=" + playlist_id_json.asString(); + if(thumbnail) { + body_item->thumbnail_url = thumbnail->url; + body_item->thumbnail_size = { thumbnail->width, thumbnail->height }; + } + return body_item; + } + + static void parse_item_section_renderer_shelf_renderer(const Json::Value &shelf_renderer_json, std::unordered_set &added_videos, BodyItems &result_items) { + if(!shelf_renderer_json.isObject()) + return; + + const Json::Value &item_content_json = shelf_renderer_json["content"]; + if(!item_content_json.isObject()) + return; + + const Json::Value &vertical_list_renderer_json = item_content_json["verticalListRenderer"]; + if(!vertical_list_renderer_json.isObject()) + return; + + const Json::Value &items_json = vertical_list_renderer_json["items"]; + if(!items_json.isArray()) + return; + + for(const Json::Value &item_json : items_json) { + if(!item_json.isObject()) + continue; + + std::shared_ptr body_item = parse_common_video_item(item_json["videoRenderer"], added_videos); + if(body_item) + result_items.push_back(std::move(body_item)); + + // TODO: Mix + //body_item = parse_playlist_renderer(item_json["radioRenderer"]); + //if(body_item) + // result_items.push_back(std::move(body_item)); + } + } + 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; @@ -732,44 +857,29 @@ namespace QuickMedia { if(!item_contents_json.isArray()) return; + std::shared_ptr body_item; for(const Json::Value &content_item_json : item_contents_json) { if(!content_item_json.isObject()) continue; - for(Json::Value::const_iterator it = content_item_json.begin(); it != content_item_json.end(); ++it) { - Json::Value key = it.key(); - if(key.isString() && strcmp(key.asCString(), "shelfRenderer") == 0) { - const Json::Value &shelf_renderer_json = *it; - if(!shelf_renderer_json.isObject()) - continue; + body_item = parse_channel_renderer(content_item_json["channelRenderer"]); + if(body_item) + result_items.push_back(std::move(body_item)); - const Json::Value &item_content_json = shelf_renderer_json["content"]; - if(!item_content_json.isObject()) - continue; + body_item = parse_playlist_renderer(content_item_json["playlistRenderer"]); + if(body_item) + result_items.push_back(std::move(body_item)); - const Json::Value &vertical_list_renderer_json = item_content_json["verticalListRenderer"]; - if(!vertical_list_renderer_json.isObject()) - continue; + // TODO: Mix + //body_item = parse_playlist_renderer(content_item_json["radioRenderer"]); + //if(body_item) + // result_items.push_back(std::move(body_item)); - const Json::Value &items_json = vertical_list_renderer_json["items"]; - if(!items_json.isArray()) - continue; - - for(const Json::Value &item_json : items_json) { - std::shared_ptr body_item = parse_content_video_renderer(item_json, added_videos); - if(body_item) - result_items.push_back(std::move(body_item)); - } - } else if(key.isString() && strcmp(key.asCString(), "channelRenderer") == 0) { - std::shared_ptr body_item = parse_channel_renderer(*it); - if(body_item) - result_items.push_back(std::move(body_item)); - } else { - std::shared_ptr body_item = parse_content_video_renderer(content_item_json, added_videos); - if(body_item) - result_items.push_back(std::move(body_item)); - } - } + parse_item_section_renderer_shelf_renderer(content_item_json["shelfRenderer"], added_videos, result_items); + + body_item = parse_common_video_item(content_item_json["videoRenderer"], added_videos); + if(body_item) + result_items.push_back(std::move(body_item)); } } @@ -919,11 +1029,11 @@ namespace QuickMedia { if(continuation_token.empty()) 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; + std::shared_ptr body_item = parse_common_video_item(item_json["gridVideoRenderer"], added_videos); + if(body_item) + body_items.push_back(std::move(body_item)); - auto body_item = parse_common_video_item(grid_video_renderer, added_videos); + body_item = parse_playlist_renderer(item_json["gridPlaylistRenderer"]); if(body_item) body_items.push_back(std::move(body_item)); } @@ -1066,7 +1176,7 @@ namespace QuickMedia { return SearchResult::ERR; for(const Json::Value &json_item : search_result_list_json) { - if(!json_item.isArray() || json_item.size() == 0) + if(!json_item.isArray() || json_item.empty()) continue; const Json::Value &search_result_json = json_item[0]; @@ -1103,6 +1213,8 @@ 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! YoutubeChannelPage::create_each_type(program, args.url, "", args.title, result_tabs); + } else if(strstr(args.url.c_str(), "&list=")) { + result_tabs.push_back(Tab{create_body(false, true), std::make_unique(program, args.url, args.title), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else { result_tabs.push_back(Tab{nullptr, std::make_unique(program, args.url), nullptr}); } @@ -1630,6 +1742,8 @@ namespace QuickMedia { return "shorts"; case YoutubeChannelPage::Type::LIVE: return "streams"; + case YoutubeChannelPage::Type::PLAYLISTS: + return "playlists"; } return ""; } @@ -1642,6 +1756,8 @@ namespace QuickMedia { return channel_name + " Shorts"; case YoutubeChannelPage::Type::LIVE: return channel_name + " Live videos"; + case YoutubeChannelPage::Type::PLAYLISTS: + return channel_name + " Playlists"; } return ""; } @@ -1651,6 +1767,7 @@ namespace QuickMedia { 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)}); + tabs.push_back(Tab{program->create_body(false, true), std::make_unique(program, url, continuation_token, title, Type::PLAYLISTS), program->create_search_bar("Search...", 350)}); } YoutubeChannelPage::YoutubeChannelPage(Program *program, std::string url, std::string continuation_token, std::string title, Type type) : @@ -1879,7 +1996,12 @@ namespace QuickMedia { PluginResult YoutubeChannelPage::submit(const SubmitArgs &args, std::vector &result_tabs) { if(args.url.empty()) return PluginResult::OK; - result_tabs.push_back(Tab{nullptr, std::make_unique(program, args.url), nullptr}); + + if(strstr(args.url.c_str(), "&list=")) { + result_tabs.push_back(Tab{create_body(false, true), std::make_unique(program, args.url, args.title), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else { + result_tabs.push_back(Tab{nullptr, std::make_unique(program, args.url), nullptr}); + } return PluginResult::OK; } @@ -2135,6 +2257,79 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult YoutubePlaylistPage::submit(const SubmitArgs &args, std::vector &result_tabs) { + result_tabs.push_back(Tab{nullptr, std::make_unique(program, args.url, true, true), nullptr}); + return PluginResult::OK; + } + + static void playlist_get_items(const Json::Value &json_root, std::unordered_set &added_videos, BodyItems &result_items) { + if(!json_root.isObject()) + return; + + const Json::Value &response_json = json_root["response"]; + if(!response_json.isObject()) + return; + + const Json::Value &contents_json = response_json["contents"]; + if(!contents_json.isObject()) + return; + + const Json::Value &tcwnr_json = contents_json["twoColumnWatchNextResults"]; + if(!tcwnr_json.isObject()) + return; + + const Json::Value &playlist_json = tcwnr_json["playlist"]; + if(!playlist_json.isObject()) + return; + + const Json::Value &playlist_inner_json = playlist_json["playlist"]; + if(!playlist_inner_json.isObject()) + return; + + //const Json::Value &title_json = playlist_inner_json["title"]; + const Json::Value &playlist_contents_json = playlist_inner_json["contents"]; + if(!playlist_contents_json.isArray()) + return; + + for(const Json::Value &content_json : playlist_contents_json) { + if(!content_json.isObject()) + continue; + + auto body_item = parse_common_video_item(content_json["playlistPanelVideoRenderer"], added_videos); + if(body_item) + result_items.push_back(std::move(body_item)); + } + } + + PluginResult YoutubePlaylistPage::lazy_fetch(BodyItems &result_items) { + std::vector additional_args = { + { "-H", "x-youtube-client-name: 1" }, + { "-H", youtube_client_version }, + }; + + std::vector cookies = get_cookies(); + additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); + + Json::Value json_root; + DownloadResult download_result = download_json(json_root, url + "&pbj=1&gl=US&hl=en", additional_args, true); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + std::unordered_set added_videos; + if(json_root.isObject()) { + playlist_get_items(json_root, added_videos, result_items); + return PluginResult::OK; + } + + if(!json_root.isArray()) + return PluginResult::OK; + + for(const Json::Value &json_item : json_root) { + playlist_get_items(json_item, added_videos, result_items); + } + + return PluginResult::OK; + } + static std::string two_column_watch_next_results_get_comments_continuation_token(const Json::Value &tcwnr_json) { const Json::Value &results_json = tcwnr_json["results"]; if(!results_json.isObject()) @@ -2192,7 +2387,9 @@ namespace QuickMedia { return; } - YoutubeVideoPage::YoutubeVideoPage(Program *program, std::string url, bool autoplay) : VideoPage(program, "", autoplay) { + YoutubeVideoPage::YoutubeVideoPage(Program *program, std::string url, bool autoplay, bool autoplay_next_item) : + VideoPage(program, "", autoplay), goto_next_item(autoplay_next_item) + { set_url(std::move(url)); } -- cgit v1.2.3