From e66d24f74d5458241d869fb3df42b4f2a2ea69f4 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 6 May 2021 08:18:38 +0200 Subject: Show youtube recommendations instead of local recommendations from related videos --- src/QuickMedia.cpp | 298 ++++++------------------------------------------ src/VideoPlayer.cpp | 40 +++++-- src/plugins/Youtube.cpp | 250 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 296 insertions(+), 292 deletions(-) (limited to 'src') diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index f94601c..594b93a 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -248,182 +248,36 @@ static sf::Color interpolate_colors(sf::Color source, sf::Color target, double p } namespace QuickMedia { - // Returns index to item or -1 if not found - static int get_body_item_by_url(Body *body, const std::string &url) { - if(url.empty()) return -1; - for(size_t i = 0; i < body->items.size(); ++i) { - auto &body_item = body->items[i]; - if(body_item->url == url) - return i; - } - return -1; - } - - static Path get_recommended_filepath(const char *plugin_name) { - Path video_history_dir = get_storage_dir().join("recommended"); - if(create_directory_recursive(video_history_dir) != 0) { - std::string err_msg = "Failed to create recommended directory "; - err_msg += video_history_dir.data; - show_notification("QuickMedia", err_msg.c_str(), Urgency::CRITICAL); - exit(1); - } - - Path video_history_filepath = video_history_dir; - return video_history_filepath.join(plugin_name).append(".json"); - } - - // TODO: Make asynchronous - static void fill_recommended_items_from_json(const char *plugin_name, const Json::Value &recommended_json, BodyItems &body_items) { - assert(recommended_json.isObject()); - - const int64_t recommendations_autodelete_period = 60*60*24*20; // 20 days - time_t time_now = time(NULL); - int num_items_deleted = 0; - - std::vector> recommended_items(recommended_json.size()); - /* TODO: Optimize member access */ - for(auto &member_name : recommended_json.getMemberNames()) { - Json::Value recommended_item = recommended_json[member_name]; - if(recommended_item.isObject()) { - Json::Value recommended_timestamp_json = recommended_item.get("recommended_timestamp", Json::Value::nullSingleton()); - Json::Value watched_timestamp_json = recommended_item.get("watched_timestamp", Json::Value::nullSingleton()); - if(watched_timestamp_json.isNumeric() && time_now - watched_timestamp_json.asInt64() >= recommendations_autodelete_period) { - ++num_items_deleted; - } else if(recommended_timestamp_json.isNumeric() && time_now - recommended_timestamp_json.asInt64() >= recommendations_autodelete_period) { - ++num_items_deleted; - } else if(recommended_timestamp_json.isNull() && watched_timestamp_json.isNull()) { - ++num_items_deleted; - } else { - recommended_items.push_back(std::make_pair(member_name, std::move(recommended_item))); - } - } - } - - if(num_items_deleted > 0) { - // TODO: Is there a better way? - Json::Value new_recommendations(Json::objectValue); - for(auto &recommended : recommended_items) { - new_recommendations[recommended.first] = recommended.second; - } - fprintf(stderr, "Number of old recommendations to delete: %d\n", num_items_deleted); - save_json_to_file_atomic(get_recommended_filepath(plugin_name), new_recommendations); - } - - /* TODO: Better algorithm for recommendations */ - std::sort(recommended_items.begin(), recommended_items.end(), [](std::pair &a, std::pair &b) { - Json::Value &a_timestamp_json = a.second["recommended_timestamp"]; - Json::Value &b_timestamp_json = b.second["recommended_timestamp"]; - int64_t a_timestamp = 0; - int64_t b_timestamp = 0; - if(a_timestamp_json.isNumeric()) - a_timestamp = a_timestamp_json.asInt64(); - if(b_timestamp_json.isNumeric()) - b_timestamp = b_timestamp_json.asInt64(); - - Json::Value &a_recommended_count_json = a.second["recommended_count"]; - Json::Value &b_recommended_count_json = b.second["recommended_count"]; - int64_t a_recommended_count = 0; - int64_t b_recommended_count = 0; - if(a_recommended_count_json.isNumeric()) - a_recommended_count = a_recommended_count_json.asInt64(); - if(b_recommended_count_json.isNumeric()) - b_recommended_count = b_recommended_count_json.asInt64(); - - /* Put frequently recommended videos on top of recommendations. Each recommendation count is worth 5 minutes */ - a_timestamp += (300 * a_recommended_count); - b_timestamp += (300 * b_recommended_count); - - return a_timestamp > b_timestamp; - }); - - for(auto it = recommended_items.begin(); it != recommended_items.end(); ++it) { - const std::string &recommended_item_id = it->first; - Json::Value &recommended_item = it->second; - - int64_t watched_count = 0; - const Json::Value &watched_count_json = recommended_item["watched_count"]; - if(watched_count_json.isNumeric()) - watched_count = watched_count_json.asInt64(); - - /* TODO: Improve recommendations with some kind of algorithm. Videos we have seen should be recommended in some cases */ - if(watched_count != 0) - continue; - - const Json::Value &recommended_title_json = recommended_item["title"]; - if(!recommended_title_json.isString()) - continue; - - auto body_item = BodyItem::create(recommended_title_json.asString()); - body_item->url = "https://www.youtube.com/watch?v=" + recommended_item_id; - body_item->thumbnail_url = "https://img.youtube.com/vi/" + recommended_item_id + "/mqdefault.jpg"; - body_item->thumbnail_size = sf::Vector2i(192, 108); - body_items.push_back(std::move(body_item)); - - // We dont want more than 150 recommendations - if(body_items.size() == 150) - break; - } - - std::random_shuffle(body_items.begin(), body_items.end()); - } - enum class HistoryType { YOUTUBE, MANGA }; - class HistoryPage : public Page { + class HistoryPage : public LazyFetchPage { public: - HistoryPage(Program *program, Page *search_page, SearchBar *search_bar, HistoryType history_type) : - Page(program), search_page(search_page), search_bar(search_bar), history_type(history_type) {} + HistoryPage(Program *program, Page *search_page, HistoryType history_type) : + LazyFetchPage(program), search_page(search_page), history_type(history_type) {} const char* get_title() const override { return "History"; } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override { return search_page->submit(title, url, result_tabs); } - void on_navigate_to_page(Body *body) override { - std::string selected_item_url = body->get_selected() ? body->get_selected()->url : ""; - body->clear_items(); + PluginResult lazy_fetch(BodyItems &result_items) override { switch(history_type) { case HistoryType::YOUTUBE: - program->youtube_get_watch_history(body->items); + program->youtube_get_watch_history(result_items); break; case HistoryType::MANGA: - program->manga_get_watch_history(program->get_plugin_name(), body->items); + program->manga_get_watch_history(program->get_plugin_name(), result_items); break; } - body->filter_search_fuzzy(search_bar->get_text()); - int item_to_revert_selection_to = get_body_item_by_url(body, selected_item_url); - if(item_to_revert_selection_to != -1) - body->set_selected_item(item_to_revert_selection_to, false); + return PluginResult::OK; } + bool reload_on_page_change() override { return true; } private: Page *search_page; - SearchBar *search_bar; HistoryType history_type; }; - class RecommendedPage : public Page { - public: - RecommendedPage(Program *program, Page *search_page, SearchBar *search_bar, const char *plugin_name) : Page(program), search_page(search_page), search_bar(search_bar), plugin_name(plugin_name) {} - const char* get_title() const override { return "Recommended"; } - PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override { - return search_page->submit(title, url, result_tabs); - } - void on_navigate_to_page(Body *body) override { - std::string selected_item_url = body->get_selected() ? body->get_selected()->url : ""; - body->clear_items(); - fill_recommended_items_from_json(plugin_name, program->load_recommended_json(), body->items); - body->filter_search_fuzzy(search_bar->get_text()); - int item_to_revert_selection_to = get_body_item_by_url(body, selected_item_url); - if(item_to_revert_selection_to != -1) - body->set_selected_item(item_to_revert_selection_to, false); - } - private: - Page *search_page; - SearchBar *search_bar; - const char *plugin_name; - }; - Program::Program() : disp(nullptr), window_size(1280, 720), @@ -978,47 +832,41 @@ namespace QuickMedia { } else if(strcmp(plugin_name, "manganelo") == 0) { tabs.push_back(Tab{create_body(), std::make_unique(this), create_search_bar("Search...", 400)}); - auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); - auto history_page = std::make_unique(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA); - tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)}); + auto history_page = std::make_unique(this, tabs.front().page.get(), HistoryType::MANGA); + tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "manganelos") == 0) { auto search_page = std::make_unique(this, plugin_name, "http://manganelos.com/"); add_manganelos_handlers(search_page.get()); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); - auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); - auto history_page = std::make_unique(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA); - tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)}); + auto history_page = std::make_unique(this, tabs.front().page.get(), HistoryType::MANGA); + tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "mangatown") == 0) { auto search_page = std::make_unique(this, plugin_name, "https://www.mangatown.com/"); add_mangatown_handlers(search_page.get()); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); - auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); - auto history_page = std::make_unique(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA); - tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)}); + auto history_page = std::make_unique(this, tabs.front().page.get(), HistoryType::MANGA); + tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "mangakatana") == 0) { auto search_page = std::make_unique(this, plugin_name, "https://mangakatana.com/", false); add_mangakatana_handlers(search_page.get()); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); - auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); - auto history_page = std::make_unique(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA); - tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)}); + auto history_page = std::make_unique(this, tabs.front().page.get(), HistoryType::MANGA); + tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "mangadex") == 0) { tabs.push_back(Tab{create_body(), std::make_unique(this), create_search_bar("Search...", 400)}); - auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); - auto history_page = std::make_unique(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA); - tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)}); + auto history_page = std::make_unique(this, tabs.front().page.get(), HistoryType::MANGA); + tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "readm") == 0) { auto search_page = std::make_unique(this, plugin_name, "https://readm.org/"); add_readm_handlers(search_page.get()); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); - auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); - auto history_page = std::make_unique(this, tabs.front().page.get(), search_bar.get(), HistoryType::MANGA); - tabs.push_back(Tab{create_body(), std::move(history_page), std::move(search_bar)}); + auto history_page = std::make_unique(this, tabs.front().page.get(), HistoryType::MANGA); + tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "manga") == 0) { auto manganelo = std::make_unique(this); auto manganelos = std::make_unique(this, "manganelos", "http://manganelos.com/"); @@ -1069,13 +917,11 @@ namespace QuickMedia { tabs.push_back(Tab{create_body(), std::make_unique(this), create_search_bar("Search...", 350)}); auto history_body = create_body(); - auto history_search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); - auto history_page = std::make_unique(this, tabs.front().page.get(), history_search_bar.get(), HistoryType::YOUTUBE); - tabs.push_back(Tab{std::move(history_body), std::move(history_page), std::move(history_search_bar)}); + auto history_page = std::make_unique(this, tabs.front().page.get(), HistoryType::YOUTUBE); + tabs.push_back(Tab{std::move(history_body), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); - auto recommended_search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); - auto recommended_page = std::make_unique(this, tabs.front().page.get(), recommended_search_bar.get(), plugin_name); - tabs.push_back(Tab{create_body(), std::move(recommended_page), std::move(recommended_search_bar)}); + auto recommended_page = std::make_unique(this); + tabs.push_back(Tab{create_body(), std::move(recommended_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "pornhub") == 0) { auto search_page = std::make_unique(this, "https://www.pornhub.com/", sf::Vector2i(320/1.5f, 180/1.5f)); add_pornhub_handlers(search_page.get()); @@ -1267,16 +1113,6 @@ namespace QuickMedia { return json_result; } - // This is not cached because we could have multiple instances of QuickMedia running the same plugin! - // TODO: Find a way to optimize this - Json::Value Program::load_recommended_json() { - Path recommended_filepath = get_recommended_filepath(plugin_name); - Json::Value json_result; - if(!read_file_as_json(recommended_filepath, json_result) || !json_result.isObject()) - json_result = Json::Value(Json::objectValue); - return json_result; - } - void Program::manga_get_watch_history(const char *plugin_name, BodyItems &history_items) { // TOOD: Make generic, instead of checking for plugin Path content_storage_dir = get_storage_dir().join(plugin_name); @@ -1559,13 +1395,16 @@ namespace QuickMedia { if(after_submit_handler) after_submit_handler(new_tabs); - for(Tab &tab : tabs) { - tab.body->clear_cache(); + for(size_t i = 0; i < tabs.size(); ++i) { + tabs[i].body->clear_cache(); + if(tabs[i].page->is_lazy_fetch_page() && static_cast(tabs[i].page.get())->reload_on_page_change()) + tab_associated_data[i].lazy_fetch_finished = false; } if(tabs[selected_tab].page->allow_submit_no_selection()) { page_loop(new_tabs, 0, after_submit_handler); } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::MANGA_IMAGES) { + page_stack.push(current_page); select_episode(selected_item.get(), false); Body *chapters_body = tabs[selected_tab].body.get(); chapters_body->filter_search_fuzzy(""); // Needed (or not really) to go to the next chapter when reaching the last page of a chapter @@ -1608,9 +1447,11 @@ namespace QuickMedia { window.setKeyRepeatEnabled(true); malloc_trim(0); } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::IMAGE_BOARD_THREAD) { + page_stack.push(current_page); current_page = PageType::IMAGE_BOARD_THREAD; image_board_thread_page(static_cast(new_tabs[0].page.get()), new_tabs[0].body.get()); } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::VIDEO) { + page_stack.push(current_page); current_page = PageType::VIDEO_CONTENT; int selected_index = tabs[selected_tab].body->get_selected_item(); video_content_page(tabs[selected_tab].page.get(), static_cast(new_tabs[0].page.get()), selected_item->get_title(), false, tabs[selected_tab].body->items, selected_index, &tab_associated_data[selected_tab].fetched_page, tab_associated_data[selected_tab].update_search_text); @@ -2004,68 +1845,6 @@ namespace QuickMedia { return true; } - void Program::save_recommendations_from_related_videos(const std::string &video_url, const std::string &video_title, const BodyItems &related_media_body_items) { - std::string video_id; - if(!youtube_url_extract_id(video_url, video_id)) { - std::string err_msg = "Failed to extract id of youtube url "; - err_msg += video_url; - err_msg + ", video wont be saved in recommendations"; - show_notification("QuickMedia", err_msg.c_str(), Urgency::LOW); - return; - } - - Json::Value recommended_json = load_recommended_json(); - time_t time_now = time(NULL); - - Json::Value &existing_recommended_json = recommended_json[video_id]; - if(existing_recommended_json.isObject()) { - int64_t watched_count = 0; - Json::Value &watched_count_json = existing_recommended_json["watched_count"]; - if(watched_count_json.isNumeric()) - watched_count = watched_count_json.asInt64(); - existing_recommended_json["watched_count"] = watched_count + 1; - existing_recommended_json["watched_timestamp"] = time_now; - } else { - Json::Value new_content_object(Json::objectValue); - new_content_object["title"] = video_title; - new_content_object["recommended_timestamp"] = time_now; - new_content_object["recommended_count"] = 1; - new_content_object["watched_count"] = 1; - new_content_object["watched_timestamp"] = time_now; - recommended_json[video_id] = std::move(new_content_object); - } - - int saved_recommendation_count = 0; - for(const auto &body_item : related_media_body_items) { - std::string recommended_video_id; - if(youtube_url_extract_id(body_item->url, recommended_video_id)) { - Json::Value &existing_recommendation = recommended_json[recommended_video_id]; - if(existing_recommendation.isObject()) { - int64_t recommended_count = 0; - Json::Value &count_json = existing_recommendation["recommended_count"]; - if(count_json.isNumeric()) - recommended_count = count_json.asInt64(); - existing_recommendation["recommended_count"] = recommended_count + 1; - existing_recommendation["recommended_timestamp"] = time_now; - } else { - Json::Value new_content_object(Json::objectValue); - new_content_object["title"] = body_item->get_title(); - new_content_object["recommended_timestamp"] = time_now; - new_content_object["recommended_count"] = 1; - recommended_json[recommended_video_id] = std::move(new_content_object); - saved_recommendation_count++; - /* TODO: Save more than the first 3 video that hasn't been watched yet? */ - if(saved_recommendation_count == 3) - break; - } - } else { - fprintf(stderr, "Failed to extract id of youtube url %s, video wont be saved in recommendations\n", video_url.c_str()); - } - } - - save_json_to_file_atomic(get_recommended_filepath(plugin_name), recommended_json); - } - static const char *useragent_str = "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"; static int accumulate_string_limit_head(char *data, int size, void *userdata) { @@ -2145,7 +1924,6 @@ namespace QuickMedia { void Program::video_content_page(Page *parent_page, VideoPage *video_page, std::string video_title, bool download_if_streaming_fails, BodyItems &next_play_items, int play_index, int *parent_body_page, const std::string &parent_page_search) { sf::Clock time_watched_timer; - bool added_recommendations = false; bool video_loaded = false; const bool is_youtube = strcmp(plugin_name, "youtube") == 0; const bool is_matrix = strcmp(plugin_name, "matrix") == 0; @@ -2224,11 +2002,10 @@ namespace QuickMedia { std::function video_event_callback; - auto load_video_error_check = [this, &related_videos, &channel_url, &video_url, &video_title, &video_player, previous_page, &time_watched_timer, &video_loaded, &added_recommendations, video_page, &video_event_callback, &on_window_create, &video_player_window, is_matrix](bool resume_video) mutable { + auto load_video_error_check = [this, &related_videos, &channel_url, &video_url, &video_title, &video_player, previous_page, &time_watched_timer, &video_loaded, video_page, &video_event_callback, &on_window_create, &video_player_window, is_matrix](bool resume_video) mutable { time_watched_timer.restart(); video_loaded = false; video_player_window = None; - added_recommendations = false; watched_videos.insert(video_url); video_player = std::make_unique(no_video, use_system_mpv_config, resume_video, is_matrix, video_event_callback, on_window_create, resources_root, get_largest_monitor_height(disp)); @@ -2394,7 +2171,7 @@ namespace QuickMedia { video_player->quit_and_save_watch_later(); while(true) { VideoPlayer::Error update_err = video_player->update(); - if(update_err != VideoPlayer::Error::OK) + if(update_err != VideoPlayer::Error::OK || !window.isOpen() || current_page == PageType::EXIT) break; std::this_thread::sleep_for(std::chrono::milliseconds(20)); } @@ -2404,6 +2181,9 @@ namespace QuickMedia { } }); + if(!window.isOpen() || current_page == PageType::EXIT) + return; + if(page_changed) { current_page = PageType::VIDEO_CONTENT; //video_player = std::make_unique(no_video, use_system_mpv_config, true, video_event_callback, on_window_create, resources_root); @@ -2500,12 +2280,6 @@ namespace QuickMedia { continue; } - /* Only save recommendations for the video if we have been watching it for 15 seconds */ - if(is_youtube && video_loaded && !added_recommendations && time_watched_timer.getElapsedTime().asSeconds() >= 15) { - added_recommendations = true; - save_recommendations_from_related_videos(video_url, video_title, related_videos); - } - if(video_player_window) { if(!cursor_visible) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp index a6f3640..5f860d2 100644 --- a/src/VideoPlayer.cpp +++ b/src/VideoPlayer.cpp @@ -63,7 +63,20 @@ namespace QuickMedia { XCloseDisplay(display); } - VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, sf::WindowHandle _parent_window, const std::string&, const std::string&) { + static std::string escape_quotes(const std::string &str) { + std::string result; + for(char c : str) { + if(c == '"') + result += "\\\""; + else if(c == '\\') + result += "\\\\"; + else + result += c; + } + return result; + } + + VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, sf::WindowHandle _parent_window, const std::string &plugin_name, const std::string &) { parent_window = _parent_window; if(!tmpnam(ipc_server_path)) { @@ -71,6 +84,10 @@ namespace QuickMedia { return Error::FAIL_TO_GENERATE_IPC_FILENAME; } + Path cookies_filepath; + if(get_cookies_filepath(cookies_filepath, plugin_name) != 0) + fprintf(stderr, "Failed to create %s cookies filepath\n", plugin_name.c_str()); + const std::string parent_window_str = std::to_string(parent_window); std::vector args; @@ -94,6 +111,8 @@ namespace QuickMedia { else ytdl_format = "--ytdl-format=bestvideo[height<=?" + std::to_string(monitor_height) + "]+bestaudio/best"; + std::string cookies_file_arg = "--cookies-file=" + cookies_filepath.data; + // TODO: Resume playback if the last video played matches the first video played next time QuickMedia is launched args.insert(args.end(), { "mpv", @@ -105,11 +124,12 @@ namespace QuickMedia { cache_dir.c_str(), watch_later_dir.c_str(), "--cache-on-disk=yes", - "--ytdl-raw-options=sub-lang=\"en,eng,enUS,en-US\",write-sub=", ytdl_format.c_str(), // TODO: Disable hr seek on low power devices? "--hr-seek=yes", "--gpu-context=x11egl", + "--cookies", + cookies_file_arg.c_str(), input_conf.c_str(), wid_arg.c_str() }); @@ -129,18 +149,14 @@ namespace QuickMedia { }); } - /* std::string ytdl_options_arg; - if(!plugin_name.empty()) { - Path cookies_filepath; - if(get_cookies_filepath(cookies_filepath, plugin_name) != 0) { - fprintf(stderr, "Warning: Failed to create %s cookies file\n", plugin_name.c_str()); - } else { - ytdl_options_arg = "--ytdl-raw-options=cookies=" + cookies_filepath.data; - args.push_back(ytdl_options_arg.c_str()); - } + if(plugin_name.empty()) { + ytdl_options_arg = "--ytdl-raw-options=sub-lang=\"en,eng,enUS,en-US\",write-sub="; + args.push_back(ytdl_options_arg.c_str()); + } else { + ytdl_options_arg = "--ytdl-raw-options=sub-lang=\"en,eng,enUS,en-US\",write-sub=,mark-watched=,cookies=\"" + escape_quotes(cookies_filepath.data) + "\""; + args.push_back(ytdl_options_arg.c_str()); } - */ if(no_video) args.push_back("--no-video"); diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 09568f6..0f5e807 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -339,12 +339,6 @@ namespace QuickMedia { return (c >= 8 && c <= 13) || c == ' '; } - static void remove_cookies_file_at_exit() { - std::lock_guard lock(cookies_mutex); - if(!cookies_filepath.empty()) - remove(cookies_filepath.c_str()); - } - // TODO: Cache this and redownload it when a network request fails with this api key? static std::string youtube_page_find_api_key() { size_t api_key_index; @@ -387,14 +381,13 @@ namespace QuickMedia { static std::vector get_cookies() { std::lock_guard lock(cookies_mutex); if(cookies_filepath.empty()) { - char filename[] = "/tmp/quickmedia.youtube.cookie.XXXXXX"; - int fd = mkstemp(filename); - if(fd == -1) - return {}; - close(fd); + Path cookies_filepath_p; + if(get_cookies_filepath(cookies_filepath_p, "youtube") != 0) { + show_notification("QuickMedia", "Failed to create youtube cookies file", Urgency::CRITICAL); + abort(); + } - cookies_filepath = filename; - atexit(remove_cookies_file_at_exit); + cookies_filepath = cookies_filepath_p.data; // TODO: Re-enable this if the api key ever changes in the future #if 0 @@ -403,10 +396,14 @@ namespace QuickMedia { api_key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"; #endif - // TODO: Is there any way to bypass this? this is needed to set VISITOR_INFO1_LIVE which is required to read comments - const char *args[] = { "curl", "-I", "-s", "-b", cookies_filepath.c_str(), "-c", cookies_filepath.c_str(), "https://www.youtube.com/subscription_manager?disable_polymer=1", nullptr }; - if(exec_program(args, nullptr, nullptr) != 0) - fprintf(stderr, "Failed to fetch cookies to view youtube comments\n"); + if(get_file_type(cookies_filepath_p) != FileType::REGULAR) { + // TODO: Is there any way to bypass this? this is needed to set VISITOR_INFO1_LIVE which is required to read comments + const char *args[] = { "curl", "-I", "-s", "-b", cookies_filepath.c_str(), "-c", cookies_filepath.c_str(), "https://www.youtube.com/subscription_manager?disable_polymer=1", nullptr }; + if(exec_program(args, nullptr, nullptr) != 0) { + show_notification("QuickMedia", "Failed to fetch cookies to view youtube comments", Urgency::CRITICAL); + abort(); + } + } } return { @@ -1451,6 +1448,191 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult YoutubeRecommendedPage::get_page(const std::string&, int page, BodyItems &result_items) { + while(current_page < page) { + PluginResult plugin_result = search_get_continuation(continuation_token, result_items); + if(plugin_result != PluginResult::OK) return plugin_result; + ++current_page; + } + return PluginResult::OK; + } + + PluginResult YoutubeRecommendedPage::search_get_continuation(const std::string ¤t_continuation_token, BodyItems &result_items) { + std::string next_url = "https://www.youtube.com/?pbj=1&ctoken=" + current_continuation_token; + + std::vector additional_args = { + { "-H", "x-spf-referer: https://www.youtube.com/" }, + { "-H", "x-youtube-client-name: 1" }, + { "-H", "x-spf-previous: https://www.youtube.com/" }, + { "-H", "x-youtube-client-version: 2.20200626.03.00" }, + { "-H", "referer: https://www.youtube.com/" } + }; + + std::vector 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; + + std::string new_continuation_token; + for(const Json::Value &json_item : json_root) { + if(!json_item.isObject()) + continue; + + const Json::Value &response_json = json_item["response"]; + if(!response_json.isObject()) + continue; + + const Json::Value &on_response_received_actions_json = response_json["onResponseReceivedActions"]; + if(!on_response_received_actions_json.isArray()) + continue; + + for(const Json::Value &response_received_command : on_response_received_actions_json) { + if(!response_received_command.isObject()) + continue; + + const Json::Value &append_continuation_items_action_json = response_received_command["appendContinuationItemsAction"]; + if(!append_continuation_items_action_json.isObject()) + continue; + + const Json::Value &continuation_items_json = append_continuation_items_action_json["continuationItems"]; + if(!continuation_items_json.isArray()) + continue; + + for(const Json::Value &content_item_json : continuation_items_json) { + if(!content_item_json.isObject()) + continue; + + if(new_continuation_token.empty()) + new_continuation_token = item_section_renderer_get_continuation_token(content_item_json); + + const Json::Value &rich_item_renderer_json = content_item_json["richItemRenderer"]; + if(!rich_item_renderer_json.isObject()) + continue; + + const Json::Value &item_content_json = rich_item_renderer_json["content"]; + if(!item_content_json.isObject()) + continue; + + const Json::Value &video_renderer_json = item_content_json["videoRenderer"]; + if(!video_renderer_json.isObject()) + continue; + + auto body_item = parse_common_video_item(video_renderer_json, added_videos); + if(body_item) + result_items.push_back(std::move(body_item)); + } + } + } + + if(!new_continuation_token.empty()) + continuation_token = std::move(new_continuation_token); + + return PluginResult::OK; + } + + PluginResult YoutubeRecommendedPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back(Tab{nullptr, std::make_unique(program, url), nullptr}); + return PluginResult::OK; + } + + PluginResult YoutubeRecommendedPage::lazy_fetch(BodyItems &result_items) { + current_page = 0; + continuation_token.clear(); + added_videos.clear(); + + std::vector additional_args = { + { "-H", "x-youtube-client-name: 1" }, + { "-H", "x-youtube-client-version: 2.20200626.03.00" } + }; + + std::vector cookies = get_cookies(); + additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); + + Json::Value json_root; + DownloadResult result = download_json(json_root, "https://www.youtube.com/?pbj=1", std::move(additional_args), true); + if(result != DownloadResult::OK) return download_result_to_plugin_result(result); + + if(!json_root.isArray()) + return PluginResult::ERR; + + std::string new_continuation_token; + for(const Json::Value &json_item : json_root) { + if(!json_item.isObject()) + continue; + + const Json::Value &response_json = json_item["response"]; + if(!response_json.isObject()) + continue; + + const Json::Value &contents_json = response_json["contents"]; + if(!contents_json.isObject()) + continue; + + const Json::Value &tcbrr_json = contents_json["twoColumnBrowseResultsRenderer"]; + if(!tcbrr_json.isObject()) + continue; + + const Json::Value &tabs_json = tcbrr_json["tabs"]; + if(!tabs_json.isArray()) + continue; + + for(const Json::Value &tab_json : tabs_json) { + if(!tab_json.isObject()) + continue; + + const Json::Value &tab_renderer_json = tab_json["tabRenderer"]; + if(!tab_renderer_json.isObject()) + continue; + + const Json::Value &content_json = tab_renderer_json["content"]; + if(!content_json.isObject()) + continue; + + const Json::Value &rich_grid_renderer_json = content_json["richGridRenderer"]; + if(!rich_grid_renderer_json.isObject()) + continue; + + const Json::Value &contents2_json = rich_grid_renderer_json["contents"]; + if(!contents2_json.isArray()) + continue; + + for(const Json::Value &content_item_json : contents2_json) { + if(!content_item_json.isObject()) + continue; + + if(new_continuation_token.empty()) + new_continuation_token = item_section_renderer_get_continuation_token(content_item_json); + + const Json::Value &rich_item_renderer_json = content_item_json["richItemRenderer"]; + if(!rich_item_renderer_json.isObject()) + continue; + + const Json::Value &item_content_json = rich_item_renderer_json["content"]; + if(!item_content_json.isObject()) + continue; + + const Json::Value &video_renderer_json = item_content_json["videoRenderer"]; + if(!video_renderer_json.isObject()) + continue; + + auto body_item = parse_common_video_item(video_renderer_json, added_videos); + if(body_item) + result_items.push_back(std::move(body_item)); + } + } + } + + if(!new_continuation_token.empty()) + continuation_token = std::move(new_continuation_token); + + return PluginResult::OK; + } + PluginResult YoutubeRelatedVideosPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { result_tabs.push_back(Tab{nullptr, std::make_unique(program, url), nullptr}); return PluginResult::OK; @@ -1512,8 +1694,8 @@ namespace QuickMedia { if(!json_item.isObject()) continue; + const Json::Value &player_response_json = json_item["playerResponse"]; if(channel_url.empty()) { - const Json::Value &player_response_json = json_item["playerResponse"]; if(player_response_json.isObject()) { const Json::Value &video_details_json = player_response_json["videoDetails"]; if(video_details_json.isObject()) { @@ -1530,6 +1712,38 @@ namespace QuickMedia { xsrf_token = xsrf_token_json.asString(); } + if(player_response_json.isObject()) { + const Json::Value &playback_tracing_json = player_response_json["playbackTracking"]; + if(playback_tracing_json.isObject()) { + if(playback_url.empty()) { + const Json::Value &video_stats_playback_url_json = playback_tracing_json["videostatsPlaybackUrl"]; + if(video_stats_playback_url_json.isObject()) { + const Json::Value &base_url_json = video_stats_playback_url_json["baseUrl"]; + if(base_url_json.isString()) + playback_url = base_url_json.asString(); + } + } + + if(watchtime_url.empty()) { + const Json::Value &video_stats_watchtime_url_json = playback_tracing_json["videostatsWatchtimeUrl"]; + if(video_stats_watchtime_url_json.isObject()) { + const Json::Value &base_url_json = video_stats_watchtime_url_json["baseUrl"]; + if(base_url_json.isString()) + watchtime_url = base_url_json.asString(); + } + } + + if(tracking_url.empty()) { + const Json::Value &p_tracking_url_json = playback_tracing_json["ptrackingUrl"]; + if(p_tracking_url_json.isObject()) { + const Json::Value &base_url_json = p_tracking_url_json["baseUrl"]; + if(base_url_json.isString()) + tracking_url = base_url_json.asString(); + } + } + } + } + const Json::Value &response_json = json_item["response"]; if(!response_json.isObject()) continue; -- cgit v1.2.3