aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2023-10-29 14:22:24 +0100
committerdec05eba <dec05eba@protonmail.com>2023-10-29 14:22:24 +0100
commit89e018c4f24a03a9436024539d10d2b058d6e956 (patch)
tree001a3aa479793d6ba7934ce75f8c6e3c74839f16 /src
parent10ecf0857b25ff2542a38015ecbe58462510efc1 (diff)
Youtube: support playlists
Diffstat (limited to 'src')
-rw-r--r--src/QuickMedia.cpp19
-rw-r--r--src/plugins/Youtube.cpp295
2 files changed, 258 insertions, 56 deletions
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<void>([&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<void>([url, video_page]() {
+ video_page->get_related_media(url);
+ });
+ } else {
+ related_videos.clear();
+ related_videos_task = AsyncTask<void>([&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<BodyItem> parse_common_video_item(const Json::Value &video_item_json, std::unordered_set<std::string> &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<BodyItem> parse_content_video_renderer(const Json::Value &content_item_json, std::unordered_set<std::string> &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<BodyItem> 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<BodyItem> 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<std::string> title = yt_json_get_text(child_video_renderer_json, "title");
+ std::optional<std::string> 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<BodyItem> 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<std::string> 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<std::string> title = yt_json_get_text(playlist_renderer_json, "title");
+
+ std::optional<Thumbnail> 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<std::string> &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<BodyItem> 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<std::string> &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<BodyItem> 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<BodyItem> 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<BodyItem> body_item = parse_channel_renderer(*it);
- if(body_item)
- result_items.push_back(std::move(body_item));
- } else {
- std::shared_ptr<BodyItem> 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<BodyItem> 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<YoutubePlaylistPage>(program, args.url, args.title), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else {
result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(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<YoutubeChannelPage>(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<YoutubeChannelPage>(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<YoutubeChannelPage>(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<YoutubeChannelPage>(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<Tab> &result_tabs) {
if(args.url.empty())
return PluginResult::OK;
- result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, args.url), nullptr});
+
+ if(strstr(args.url.c_str(), "&list=")) {
+ result_tabs.push_back(Tab{create_body(false, true), std::make_unique<YoutubePlaylistPage>(program, args.url, args.title), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ } else {
+ result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, args.url), nullptr});
+ }
return PluginResult::OK;
}
@@ -2135,6 +2257,79 @@ namespace QuickMedia {
return PluginResult::OK;
}
+ PluginResult YoutubePlaylistPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, args.url, true, true), nullptr});
+ return PluginResult::OK;
+ }
+
+ static void playlist_get_items(const Json::Value &json_root, std::unordered_set<std::string> &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<CommandArg> additional_args = {
+ { "-H", "x-youtube-client-name: 1" },
+ { "-H", youtube_client_version },
+ };
+
+ std::vector<CommandArg> 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<std::string> 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));
}