diff options
author | dec05eba <dec05eba@protonmail.com> | 2021-03-10 23:25:09 +0100 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2021-03-10 23:27:28 +0100 |
commit | 5d29f22093fd602bc4d8863208e7812c0746e62e (patch) | |
tree | 844ee610b45880873e7afe17cb563f35e8982af5 | |
parent | 1fe31ba2e244d9ae26d1f8d00f411713d2eaacf7 (diff) |
Youtube: add youtube comments to ctrl+r
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | TODO | 3 | ||||
-rw-r--r-- | include/QuickMedia.hpp | 3 | ||||
-rw-r--r-- | plugins/Page.hpp | 1 | ||||
-rw-r--r-- | plugins/Youtube.hpp | 30 | ||||
-rw-r--r-- | src/Body.cpp | 1 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 44 | ||||
-rw-r--r-- | src/plugins/Spotify.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Youtube.cpp | 412 |
9 files changed, 454 insertions, 46 deletions
@@ -34,8 +34,8 @@ Press `ESC` to go back to the previous menu.\ Press `ESC` or `Backspace` to close the video.\ Press `Ctrl + F` to switch between window mode and fullscreen mode when watching a video.\ Press `Space` to pause/unpause a video.\ -Press `Ctrl + R` to show related/channels video menu when watching a video (if supported).\ -Press `Ctrl + T` when hovering over a manga chapter to start tracking manga after that chapter. This only works if AutoMedia is installed and accessible in PATH environment variable.\ +Press `Ctrl + R` to show video comments, related videos or video channel when watching a video (if supported).\ +Press `Ctrl + T` when hovering over a manga chapter to start tracking manga after that chapter. This only works if AutoMedia is installed. Press `Backspace` to return to the preview item when reading replies in image board threads.\ Press `R` to paste the post number of the selected post into the post field (image boards).\ Press `I` to begin writing a post to a thread (image boards), press `ESC` to cancel.\ @@ -159,3 +159,6 @@ Add an option to select video resolution, if we want to use less power and less Use mpv option --gpu-context=x11egl on pinephone to force xwayland on wayland, to be able to embed the mpv window inside the quickmedia. Replies to the local user shouldn't remove the red text. Maybe fix this by checking if the reply to message user is the local user or when the replied to message has loaded then make the reply red if its a reply to us. Also for existing messages check if the message is a notification message and then make the message red. Sort reactions by timestamp. +Check what happens with xsrf_token if comments are not fetched for a long time. Does it time out? if so do we need to refetch the video page to get the new token?. +Add support for comments in live youtube videos, api is at: https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8. +Make video visible when reading comments (youtube).
\ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 5203c5b..512a722 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -96,7 +96,8 @@ namespace QuickMedia { private: void base_event_handler(sf::Event &event, PageType previous_page, Body *body, SearchBar *search_bar, bool handle_key_press = true, bool handle_searchbar = true); void page_loop_render(sf::RenderWindow &window, std::vector<Tab> &tabs, int selected_tab, TabAssociatedData &tab_associated_data, const Json::Value *json_chapters); - void page_loop(std::vector<Tab> &tabs, int start_tab_index = 0, std::function<void()> after_submit_handler = nullptr); + using PageLoopSubmitHandler = std::function<void(const std::vector<Tab> &new_tabs)>; + void page_loop(std::vector<Tab> &tabs, int start_tab_index = 0, PageLoopSubmitHandler after_submit_handler = nullptr); void video_content_page(VideoPage *video_page, std::string video_url, std::string video_title, bool download_if_streaming_fails); // Returns -1 to go to previous chapter, 0 to stay on same chapter and 1 to go to next chapter int image_page(MangaImagesPage *images_page, Body *chapters_body); diff --git a/plugins/Page.hpp b/plugins/Page.hpp index e720f14..44526db 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -101,6 +101,7 @@ namespace QuickMedia { virtual PageTypez get_type() const override { return PageTypez::VIDEO; } virtual BodyItems get_related_media(const std::string &url, std::string &channel_url) { (void)url; (void)channel_url; return {}; } virtual std::unique_ptr<Page> create_search_page(Program *program, int &search_delay) { (void)program; (void)search_delay; return nullptr; } + virtual std::unique_ptr<Page> create_comments_page(Program *program) { (void)program; return nullptr; } // Return nullptr if the service doesn't support related videos page virtual std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) = 0; // Return nullptr if the service doesn't support channels page diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index c0bb429..f8a5d5f 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -21,6 +21,32 @@ namespace QuickMedia { std::unordered_set<std::string> added_videos; }; + class YoutubeCommentsPage : public LazyFetchPage { + public: + YoutubeCommentsPage(Program *program, const std::string &xsrf_token, const std::string &continuation_token) : LazyFetchPage(program), xsrf_token(xsrf_token), continuation_token(continuation_token) {} + const char* get_title() const override { return "Comments"; } + PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + PluginResult lazy_fetch(BodyItems &result_items) override; + private: + int current_page = 0; + std::string xsrf_token; + std::string continuation_token; + }; + + class YoutubeCommentRepliesPage : public LazyFetchPage { + public: + YoutubeCommentRepliesPage(Program *program, const std::string &xsrf_token, const std::string &continuation_token) : LazyFetchPage(program), xsrf_token(xsrf_token), continuation_token(continuation_token) {} + const char* get_title() const override { return "Comment replies"; } + PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + PluginResult lazy_fetch(BodyItems &result_items) override; + private: + int current_page = 0; + std::string xsrf_token; + std::string continuation_token; + }; + class YoutubeChannelPage : public LazyFetchPage { public: YoutubeChannelPage(Program *program, std::string url, std::string continuation_token, std::string title) : LazyFetchPage(program), url(std::move(url)), continuation_token(std::move(continuation_token)), title(std::move(title)) {} @@ -53,7 +79,11 @@ namespace QuickMedia { const char* get_title() const override { return ""; } BodyItems get_related_media(const std::string &url, std::string &channel_url) override; std::unique_ptr<Page> create_search_page(Program *program, int &search_delay) override; + std::unique_ptr<Page> create_comments_page(Program *program) override; std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) override; std::unique_ptr<LazyFetchPage> create_channels_page(Program *program, const std::string &channel_url) override; + private: + std::string xsrf_token; + std::string comments_continuation_token; }; }
\ No newline at end of file diff --git a/src/Body.cpp b/src/Body.cpp index ca91566..8a32f76 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -663,6 +663,7 @@ namespace QuickMedia { update_dirty_state(item, size.x); item->last_drawn_time = draw_timer.getElapsedTime().asMilliseconds(); sf::Vector2u window_size = window.getSize(); + get_item_height(item, size.x, true, false); glEnable(GL_SCISSOR_TEST); glScissor(pos.x, (int)window_size.y - (int)pos.y - (int)size.y, size.x, size.y); draw_item(window, item, pos, size, size.y + spacing_y, -1, Json::Value::nullSingleton(), include_embedded_item); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 7e758b9..9f8c7bd 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -549,7 +549,7 @@ namespace QuickMedia { } if(use_tor && !is_program_executable_by_name("torsocks")) { - fprintf(stderr, "torsocks needs to be installed (and accessible from PATH environment variable) when using the --tor option\n"); + fprintf(stderr, "torsocks needs to be installed when using the --tor option\n"); return -2; } @@ -560,7 +560,7 @@ namespace QuickMedia { } if(!is_program_executable_by_name("waifu2x-ncnn-vulkan")) { - fprintf(stderr, "waifu2x-ncnn-vulkan needs to be installed (and accessible from PATH environment variable) when using the --upscale-images/--upscale-images-always option\n"); + fprintf(stderr, "waifu2x-ncnn-vulkan needs to be installed when using the --upscale-images/--upscale-images-always option\n"); return -2; } @@ -1023,7 +1023,7 @@ namespace QuickMedia { } } - void Program::page_loop(std::vector<Tab> &tabs, int start_tab_index, std::function<void()> after_submit_handler) { + void Program::page_loop(std::vector<Tab> &tabs, int start_tab_index, PageLoopSubmitHandler after_submit_handler) { if(tabs.empty()) { show_notification("QuickMedia", "No tabs provided!", Urgency::CRITICAL); return; @@ -1077,9 +1077,6 @@ namespace QuickMedia { return; } - if(after_submit_handler) - after_submit_handler(); - if(tabs[selected_tab].page->clear_search_after_submit() && tabs[selected_tab].search_bar) { if(!tabs[selected_tab].search_bar->get_text().empty()) { tabs[selected_tab].search_bar->clear(); @@ -1103,6 +1100,9 @@ namespace QuickMedia { if(new_tabs.empty()) return; + if(after_submit_handler) + after_submit_handler(new_tabs); + for(Tab &tab : tabs) { tab.body->clear_cache(); } @@ -1407,7 +1407,7 @@ namespace QuickMedia { associated_data.lazy_fetch_finished = true; FetchResult fetch_result = associated_data.fetch_future.get(); tabs[i].body->items = std::move(fetch_result.body_items); - tabs[i].body->filter_search_fuzzy(tabs[i].search_bar->get_text()); + if(tabs[i].search_bar) tabs[i].body->filter_search_fuzzy(tabs[i].search_bar->get_text()); if(fetch_result.result != PluginResult::OK) associated_data.search_result_text.setString("Failed to fetch page!"); else if(tabs[i].body->items.empty()) @@ -1760,7 +1760,7 @@ namespace QuickMedia { current_page = previous_page; } else { channel_url.clear(); - // TODO: Make async. What if the server is frozen? + // TODO: Remove this and use lazy_fetch instead related_videos = video_page->get_related_media(video_url, channel_url); // TODO: Make this also work for other video plugins @@ -1880,6 +1880,7 @@ namespace QuickMedia { int search_delay = 0; auto search_page = video_page->create_search_page(this, search_delay); + auto comments_page = video_page->create_comments_page(this); auto related_videos_page = video_page->create_related_videos_page(this, video_url, video_title); auto channels_page = video_page->create_channels_page(this, channel_url); if(search_page || related_videos_page || channels_page) { @@ -1890,6 +1891,9 @@ namespace QuickMedia { if(search_page) { tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", search_delay)}); } + if(comments_page) { + tabs.push_back(Tab{create_body(), std::move(comments_page), nullptr}); + } if(related_videos_page) { auto related_videos_body = create_body(); related_videos_body->items = related_videos; @@ -1900,19 +1904,21 @@ namespace QuickMedia { } bool page_changed = false; - page_loop(tabs, 1, [this, &video_player, &page_changed]() { - window.setMouseCursorVisible(true); - if(video_player) { - video_player->quit_and_save_watch_later(); - while(true) { - VideoPlayer::Error update_err = video_player->update(); - if(update_err != VideoPlayer::Error::OK) - break; - std::this_thread::sleep_for(std::chrono::milliseconds(20)); + page_loop(tabs, 1, [this, &video_player, &page_changed](const std::vector<Tab> &new_tabs) { + if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::VIDEO) { + window.setMouseCursorVisible(true); + if(video_player) { + video_player->quit_and_save_watch_later(); + while(true) { + VideoPlayer::Error update_err = video_player->update(); + if(update_err != VideoPlayer::Error::OK) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + video_player.reset(); } - video_player.reset(); + page_changed = true; } - page_changed = true; }); if(page_changed) { diff --git a/src/plugins/Spotify.cpp b/src/plugins/Spotify.cpp index 3ce640a..14f9831 100644 --- a/src/plugins/Spotify.cpp +++ b/src/plugins/Spotify.cpp @@ -167,7 +167,7 @@ namespace QuickMedia { if(result != PluginResult::OK) return result; - result_tabs.push_back(Tab{std::move(body), std::move(episode_list_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + result_tabs.push_back(Tab{std::move(body), std::move(episode_list_page), nullptr}); return result; } diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index b3c9a60..ced6c45 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -5,6 +5,7 @@ #include "../../include/Scale.hpp" #include <json/writer.h> #include <string.h> +#include <unistd.h> namespace QuickMedia { // This is a common setup of text in the youtube json @@ -18,7 +19,7 @@ namespace QuickMedia { const Json::Value &simple_text_json = text_json["simpleText"]; if(simple_text_json.isString()) { - return simple_text_json.asCString(); + return simple_text_json.asString(); } else { const Json::Value &runs_json = text_json["runs"]; if(!runs_json.isArray() || runs_json.empty()) @@ -46,8 +47,13 @@ namespace QuickMedia { int height; }; + enum class ThumbnailSize { + SMALLEST, + LARGEST + }; + // TODO: Use this in |parse_common_video_item| when QuickMedia supports webp - static std::optional<Thumbnail> yt_json_get_largest_thumbnail(const Json::Value &thumbnail_json) { + static std::optional<Thumbnail> yt_json_get_thumbnail(const Json::Value &thumbnail_json, ThumbnailSize thumbnail_size) { if(!thumbnail_json.isObject()) return std::nullopt; @@ -75,11 +81,22 @@ namespace QuickMedia { thumbnails.push_back({ url_json.asCString(), width_json.asInt(), height_json.asInt() }); } - return *std::max_element(thumbnails.begin(), thumbnails.end(), [](const Thumbnail &thumbnail1, const Thumbnail &thumbnail2) { - int size1 = thumbnail1.width * thumbnail1.height; - int size2 = thumbnail2.width * thumbnail2.height; - return size1 < size2; - }); + switch(thumbnail_size) { + case ThumbnailSize::SMALLEST: + return *std::min_element(thumbnails.begin(), thumbnails.end(), [](const Thumbnail &thumbnail1, const Thumbnail &thumbnail2) { + int size1 = thumbnail1.width * thumbnail1.height; + int size2 = thumbnail2.width * thumbnail2.height; + return size1 < size2; + }); + case ThumbnailSize::LARGEST: + return *std::max_element(thumbnails.begin(), thumbnails.end(), [](const Thumbnail &thumbnail1, const Thumbnail &thumbnail2) { + int size1 = thumbnail1.width * thumbnail1.height; + int size2 = thumbnail2.width * thumbnail2.height; + return size1 < size2; + }); + } + + return std::nullopt; } static std::shared_ptr<BodyItem> parse_common_video_item(const Json::Value &video_item_json, std::unordered_set<std::string> &added_videos) { @@ -196,7 +213,7 @@ namespace QuickMedia { std::optional<std::string> subscribers = yt_json_get_text(channel_renderer_json, "subscriberCountText"); const Json::Value &thumbnail_json = channel_renderer_json["thumbnail"]; - std::optional<Thumbnail> thumbnail = yt_json_get_largest_thumbnail(thumbnail_json); + std::optional<Thumbnail> thumbnail = yt_json_get_thumbnail(thumbnail_json, ThumbnailSize::LARGEST); auto body_item = BodyItem::create(title.value()); std::string desc; @@ -300,6 +317,37 @@ namespace QuickMedia { } } + static std::mutex cookies_mutex; + static std::string cookies_filepath; + + static void remove_cookies_file_at_exit() { + std::lock_guard<std::mutex> lock(cookies_mutex); + if(!cookies_filepath.empty()) + remove(cookies_filepath.c_str()); + } + + static std::vector<CommandArg> get_cookies() { + std::lock_guard<std::mutex> lock(cookies_mutex); + if(cookies_filepath.empty()) { + char filename[] = "quickmedia.youtube.cookie.XXXXXX"; + int fd = mkstemp(filename); + if(fd == -1) + return {}; + close(fd); + + cookies_filepath = "/tmp/"; + cookies_filepath += filename; + cookies_filepath += ".txt"; + + atexit(remove_cookies_file_at_exit); + } + + return { + CommandArg{ "-b", cookies_filepath }, + CommandArg{ "-c", cookies_filepath } + }; + } + static std::string remove_index_from_playlist_url(const std::string &url) { std::string result = url; size_t index = result.rfind("&index="); @@ -316,6 +364,30 @@ namespace QuickMedia { return parse_common_video_item(compact_video_renderer_json, added_videos); } + static std::string item_section_renderer_get_continuation(const Json::Value &item_section_renderer) { + if(!item_section_renderer.isObject()) + return ""; + + const Json::Value &continuations_json = item_section_renderer["continuations"]; + if(!continuations_json.isArray()) + return ""; + + for(const Json::Value &json_item : continuations_json) { + if(!json_item.isObject()) + continue; + + const Json::Value &next_continuation_data_json = json_item["nextContinuationData"]; + if(!next_continuation_data_json.isObject()) + continue; + + const Json::Value &continuation_json = next_continuation_data_json["continuation"]; + if(continuation_json.isString()) + return continuation_json.asString(); + } + + return ""; + } + static BodyItems parse_channel_videos(const Json::Value &json_root, std::string &continuation_token, std::unordered_set<std::string> &added_videos) { BodyItems body_items; if(!json_root.isArray()) @@ -450,8 +522,8 @@ namespace QuickMedia { { "-H", "referer: " + search_url } }; - //std::vector<CommandArg> cookies = get_cookies(); - //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); + std::vector<CommandArg> cookies = get_cookies(); + additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; DownloadResult result = download_json(json_root, search_url + "&pbj=1", std::move(additional_args), true); @@ -519,8 +591,8 @@ namespace QuickMedia { { "-H", "referer: " + url } }; - //std::vector<CommandArg> cookies = get_cookies(); - //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); + std::vector<CommandArg> 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); @@ -578,6 +650,263 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult YoutubeCommentsPage::get_page(const std::string&, int page, BodyItems &result_items) { + while(current_page < page) { + PluginResult plugin_result = lazy_fetch(result_items); + if(plugin_result != PluginResult::OK) return plugin_result; + ++current_page; + } + return PluginResult::OK; + } + + PluginResult YoutubeCommentsPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) { + if(url.empty()) + return PluginResult::OK; + result_tabs.push_back(Tab{create_body(), std::make_unique<YoutubeCommentRepliesPage>(program, xsrf_token, url), nullptr}); + return PluginResult::OK; + } + + static std::string comment_thread_renderer_get_replies_continuation(const Json::Value &comment_thread_renderer_json) { + if(!comment_thread_renderer_json.isObject()) + return ""; + + const Json::Value &replies_json = comment_thread_renderer_json["replies"]; + if(!replies_json.isObject()) + return ""; + + const Json::Value &comment_replies_renderer = replies_json["commentRepliesRenderer"]; + if(!comment_replies_renderer.isObject()) + return ""; + + // item_section_renderer_get_continuation is compatible with commentRepliesRenderer + return item_section_renderer_get_continuation(comment_replies_renderer); + } + + // Returns empty string if comment is not hearted + static std::string comment_renderer_get_hearted_tooltip(const Json::Value &comment_renderer_json) { + const Json::Value &action_buttons_json = comment_renderer_json["actionButtons"]; + if(!action_buttons_json.isObject()) + return ""; + + const Json::Value &comment_action_buttons_renderer_json = action_buttons_json["commentActionButtonsRenderer"]; + if(!comment_action_buttons_renderer_json.isObject()) + return ""; + + const Json::Value &creator_heart_json = comment_action_buttons_renderer_json["creatorHeart"]; + if(!creator_heart_json.isObject()) + return ""; + + const Json::Value &creator_heart_renderer_json = creator_heart_json["creatorHeartRenderer"]; + if(!creator_heart_renderer_json.isObject()) + return ""; + + const Json::Value &hearted_tooltip_json = creator_heart_renderer_json["heartedTooltip"]; + if(!hearted_tooltip_json.isString()) + return ""; + + return hearted_tooltip_json.asString(); + } + + static std::shared_ptr<BodyItem> comment_renderer_to_body_item(const Json::Value &comment_renderer_json) { + if(!comment_renderer_json.isObject()) + return nullptr; + + std::optional<std::string> author_text = yt_json_get_text(comment_renderer_json, "authorText"); + if(!author_text) + return nullptr; + + std::string title = author_text.value(); + std::optional<std::string> published_time_text = yt_json_get_text(comment_renderer_json, "publishedTimeText"); + if(published_time_text) + title += " - " + published_time_text.value(); + + auto body_item = BodyItem::create(std::move(title)); + std::string description; + + const Json::Value &author_is_channel_owner_json = comment_renderer_json["authorIsChannelOwner"]; + if(author_is_channel_owner_json.isBool() && author_is_channel_owner_json.asBool()) + body_item->set_title_color(sf::Color(150, 255, 150)); + + std::optional<std::string> comment = yt_json_get_text(comment_renderer_json, "contentText"); + if(comment) + description = comment.value(); + + std::optional<Thumbnail> thumbnail = yt_json_get_thumbnail(comment_renderer_json["authorThumbnail"], ThumbnailSize::SMALLEST); + if(thumbnail) { + body_item->thumbnail_url = thumbnail->url; + body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; + body_item->thumbnail_size.x = thumbnail->width; + body_item->thumbnail_size.y = thumbnail->height; + body_item->thumbnail_size = body_item->thumbnail_size; + } + + const Json::Value &like_count_json = comment_renderer_json["likeCount"]; + if(like_count_json.isInt64()) { + if(!description.empty()) + description += '\n'; + description += "👍 " + std::to_string(like_count_json.asInt64()); + } + + const Json::Value &reply_count_json = comment_renderer_json["replyCount"]; + if(reply_count_json.isInt64()) { + if(!description.empty()) + description += '\n'; + description += std::to_string(reply_count_json.asInt64()) + " replies"; + } + + std::string hearted_tooltip = comment_renderer_get_hearted_tooltip(comment_renderer_json); + if(!hearted_tooltip.empty()) { + if(!description.empty()) + description += ' '; + description += std::move(hearted_tooltip); + } + + body_item->set_description(std::move(description)); + return body_item; + } + + PluginResult YoutubeCommentsPage::lazy_fetch(BodyItems &result_items) { + std::string next_url = "https://www.youtube.com/comment_service_ajax?action_get_comments=1&pbj=1&ctoken="; + next_url += url_param_encode(continuation_token); + //next_url += "&continuation="; + //next_url += url_param_encode(comments_continuation_token); + next_url += "&type=next"; + + std::vector<CommandArg> additional_args = { + { "-H", "x-youtube-client-name: 1" }, + { "-H", "x-youtube-client-version: 2.20210308.08.00" }, + { "-F", "session_token=" + xsrf_token } + }; + + std::vector<CommandArg> 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_plugin_result(result); + + if(!json_root.isObject()) + return PluginResult::ERR; + + const Json::Value &xsrf_token_json = json_root["xsrf_token"]; + if(xsrf_token_json.isString()) + xsrf_token = xsrf_token_json.asString(); + + const Json::Value &response_json = json_root["response"]; + if(!response_json.isObject()) + return PluginResult::ERR; + + const Json::Value &continuation_contents_json = response_json["continuationContents"]; + if(!continuation_contents_json.isObject()) + return PluginResult::ERR; + + const Json::Value &item_section_continuation_json = continuation_contents_json["itemSectionContinuation"]; + if(!item_section_continuation_json.isObject()) + return PluginResult::ERR; + + const Json::Value &contents_json = item_section_continuation_json["contents"]; + if(contents_json.isArray()) { + for(const Json::Value &json_item : contents_json) { + if(!json_item.isObject()) + continue; + + const Json::Value &comment_thread_renderer = json_item["commentThreadRenderer"]; + if(!comment_thread_renderer.isObject()) + continue; + + const Json::Value &comment_json = comment_thread_renderer["comment"]; + if(!comment_json.isObject()) + continue; + + auto body_item = comment_renderer_to_body_item(comment_json["commentRenderer"]); + if(body_item) { + body_item->url = comment_thread_renderer_get_replies_continuation(comment_thread_renderer); + result_items.push_back(std::move(body_item)); + } + } + } + + continuation_token = item_section_renderer_get_continuation(item_section_continuation_json); + + return PluginResult::OK; + } + + PluginResult YoutubeCommentRepliesPage::get_page(const std::string&, int page, BodyItems &result_items) { + while(current_page < page) { + PluginResult plugin_result = lazy_fetch(result_items); + if(plugin_result != PluginResult::OK) return plugin_result; + ++current_page; + } + return PluginResult::OK; + } + + PluginResult YoutubeCommentRepliesPage::submit(const std::string&, const std::string&, std::vector<Tab>&) { + return PluginResult::OK; + } + + PluginResult YoutubeCommentRepliesPage::lazy_fetch(BodyItems &result_items) { + std::string next_url = "https://www.youtube.com/comment_service_ajax?action_get_comment_replies=1&pbj=1&ctoken="; + next_url += url_param_encode(continuation_token); + //next_url += "&continuation="; + //next_url += url_param_encode(comments_continuation_token); + next_url += "&type=next"; + + std::vector<CommandArg> additional_args = { + { "-H", "x-youtube-client-name: 1" }, + { "-H", "x-youtube-client-version: 2.20210308.08.00" }, + { "-F", "session_token=" + xsrf_token } + }; + + std::vector<CommandArg> 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_plugin_result(result); + + if(!json_root.isArray()) + return PluginResult::ERR; + + for(const Json::Value &json_item : json_root) { + if(!json_item.isObject()) + continue; + + const Json::Value &xsrf_token_json = json_item["xsrf_token"]; + if(xsrf_token_json.isString()) + xsrf_token = xsrf_token_json.asString(); + + 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 &comment_replies_continuation_json = continuation_contents_json["commentRepliesContinuation"]; + if(!comment_replies_continuation_json.isObject()) + continue; + + const Json::Value &contents_json = comment_replies_continuation_json["contents"]; + if(contents_json.isArray()) { + for(const Json::Value &content_item_json : contents_json) { + if(!content_item_json.isObject()) + continue; + + auto body_item = comment_renderer_to_body_item(content_item_json["commentRenderer"]); + if(body_item) + result_items.push_back(std::move(body_item)); + } + } + + // item_section_renderer_get_continuation is compatible with commentRepliesContinuation + continuation_token = item_section_renderer_get_continuation(comment_replies_continuation_json); + return PluginResult::OK; + } + + return PluginResult::ERR; + } + static std::string channel_url_extract_id(const std::string &channel_url) { size_t index = channel_url.find("channel/"); if(index == std::string::npos) @@ -634,8 +963,8 @@ namespace QuickMedia { { "--data-raw", Json::writeString(json_builder, request_json) } }; - //std::vector<CommandArg> cookies = get_cookies(); - //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); + std::vector<CommandArg> 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); @@ -717,8 +1046,8 @@ namespace QuickMedia { { "--data-raw", Json::writeString(json_builder, request_json) } }; - //std::vector<CommandArg> cookies = get_cookies(); - //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); + std::vector<CommandArg> 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); @@ -768,7 +1097,7 @@ namespace QuickMedia { return PluginResult::OK; } - PluginResult YoutubeChannelPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) { + PluginResult YoutubeChannelPage::submit(const std::string&, const std::string&, std::vector<Tab> &result_tabs) { if(url.empty()) return PluginResult::OK; result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program), nullptr}); @@ -785,8 +1114,8 @@ namespace QuickMedia { { "-H", "referer: " + url } }; - //std::vector<CommandArg> cookies = get_cookies(); - //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); + std::vector<CommandArg> cookies = get_cookies(); + additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; DownloadResult result = download_json(json_root, url + "/videos?pbj=1", std::move(additional_args), true); @@ -800,8 +1129,32 @@ namespace QuickMedia { return PluginResult::OK; } - // TODO: Make this faster by using string search instead of parsing html. - // TODO: If the result is a play + 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()) + return ""; + + const Json::Value &results2_json = results_json["results"]; + if(!results2_json.isObject()) + return ""; + + const Json::Value &contents_json = results2_json["contents"]; + if(!contents_json.isArray()) + return ""; + + std::string comments_continuation_token; + for(const Json::Value &content_item_json : contents_json) { + if(!content_item_json.isObject()) + continue; + + comments_continuation_token = item_section_renderer_get_continuation(content_item_json["itemSectionRenderer"]); + if(!comments_continuation_token.empty()) + return comments_continuation_token; + } + + return ""; + } + BodyItems YoutubeVideoPage::get_related_media(const std::string &url, std::string &channel_url) { BodyItems result_items; @@ -814,8 +1167,8 @@ namespace QuickMedia { { "-H", "referer: " + url } }; - //std::vector<CommandArg> cookies = get_cookies(); - //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); + std::vector<CommandArg> cookies = get_cookies(); + additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; DownloadResult result = download_json(json_root, modified_url + "&pbj=1", std::move(additional_args), true); @@ -842,6 +1195,12 @@ namespace QuickMedia { } } + if(xsrf_token.empty()) { + const Json::Value &xsrf_token_json = json_item["xsrf_token"]; + if(xsrf_token_json.isString()) + xsrf_token = xsrf_token_json.asString(); + } + const Json::Value &response_json = json_item["response"]; if(!response_json.isObject()) continue; @@ -854,6 +1213,9 @@ namespace QuickMedia { if(!tcwnr_json.isObject()) return result_items; + if(comments_continuation_token.empty()) + comments_continuation_token = two_column_watch_next_results_get_comments_continuation_token(tcwnr_json); + const Json::Value &secondary_results_json = tcwnr_json["secondaryResults"]; if(!secondary_results_json.isObject()) return result_items; @@ -901,6 +1263,10 @@ namespace QuickMedia { return std::make_unique<YoutubeSearchPage>(program); } + std::unique_ptr<Page> YoutubeVideoPage::create_comments_page(Program *program) { + return std::make_unique<YoutubeCommentsPage>(program, xsrf_token, comments_continuation_token); + } + std::unique_ptr<RelatedVideosPage> YoutubeVideoPage::create_related_videos_page(Program *program, const std::string&, const std::string&) { return std::make_unique<YoutubeRelatedVideosPage>(program); } |