diff options
-rw-r--r-- | TODO | 2 | ||||
-rw-r--r-- | include/QuickMedia.hpp | 3 | ||||
-rw-r--r-- | plugins/Page.hpp | 1 | ||||
-rw-r--r-- | plugins/Youtube.hpp | 20 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 298 | ||||
-rw-r--r-- | src/VideoPlayer.cpp | 40 | ||||
-rw-r--r-- | src/plugins/Youtube.cpp | 250 |
7 files changed, 317 insertions, 297 deletions
@@ -27,7 +27,6 @@ Add ctrl+c keybiding to copy the url to the currently selected post on 4chan. Add ctrl+s to save the previewing image/video (for images that would be a copy, since its already stored in cache and for videos youtube-dl would be used). Use https://github.com/simdjson/simdjson as a json library in other parts than matrix. Sanitize check: do not allow pasting more than 2gb of text. -Only add related videos to recommendations if its the first time we watch the video. This is to prevent rewatching a video multiple times from messing up recommendations. Implement mentions in matrix with an autofill list, like on element. Also do the same with / commands. Add option to disable autosearch and search when pressing enter instead or something? this would be needed for mobile phones where typing is slow. Render view to a rendertexture and render that instead of redrawing every time every time. @@ -115,7 +114,6 @@ Replace sfml font glyph loading completely with FreeType. There is a very old bu Add arguments to pipe plugin to pass input and output fifo for sending commands to QuickMedia and receiving events. Create a workaround for dwm terminal swallow patch stealing mpv when moving QuickMedia to another monitor sometimes. Maybe check for structure notify events on mpv and reparent and select input on the mpv window again? Add option to decline and mute user in invites. This is to combat invite spam, where muted users cant invite you. -Allow hiding videos so they dont show up in recommendations and related videos. Add an option to select video resolution, if we want to use less power and less bandwidth for example. 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. diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index c01b4b8..492003f 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -97,7 +97,6 @@ namespace QuickMedia { void youtube_get_watch_history(BodyItems &history_items); Json::Value load_video_history_json(); - Json::Value load_recommended_json(); private: void init(Window parent_window); void load_plugin_by_name(std::vector<Tab> &tabs, const char *start_dir, int &start_tab_index); @@ -131,8 +130,6 @@ namespace QuickMedia { // Returns PageType::EXIT if empty PageType pop_page_stack(); - - void save_recommendations_from_related_videos(const std::string &video_url, const std::string &video_title, const BodyItems &related_media_body_items); private: enum class UpscaleImageAction { NO, diff --git a/plugins/Page.hpp b/plugins/Page.hpp index 175b44b..426469a 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -90,6 +90,7 @@ namespace QuickMedia { virtual PluginResult lazy_fetch(BodyItems &result_items) = 0; // If this returns true then |lazy_fetch| is not meant to return results but async background load the page. This can be used to fetch API keys for example virtual bool lazy_fetch_is_loader() { return false; } + virtual bool reload_on_page_change() { return false; } }; class RelatedVideosPage : public Page { diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index dc3f0e7..746619c 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -86,6 +86,22 @@ namespace QuickMedia { std::array<AsyncTask<std::vector<YoutubeSubscriptionTaskResult>>, 4> subscription_load_tasks; // TODO: Use multiple curl outputs instead? }; + class YoutubeRecommendedPage : public LazyFetchPage { + public: + YoutubeRecommendedPage(Program *program) : LazyFetchPage(program) {} + const char* get_title() const override { return "Recommended"; } + 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; + bool reload_on_page_change() override { return true; } + private: + PluginResult search_get_continuation(const std::string &continuation_token, BodyItems &result_items); + private: + int current_page = 0; + std::string continuation_token; + std::unordered_set<std::string> added_videos; + }; + class YoutubeRelatedVideosPage : public RelatedVideosPage { public: YoutubeRelatedVideosPage(Program *program) : RelatedVideosPage(program) {} @@ -106,5 +122,9 @@ namespace QuickMedia { std::string xsrf_token; std::string comments_continuation_token; std::string url; + + std::string playback_url; + std::string watchtime_url; + std::string tracking_url; }; }
\ No newline at end of file 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<std::pair<std::string, Json::Value>> 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<std::string, Json::Value> &a, std::pair<std::string, Json::Value> &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<Tab> &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<Tab> &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<ManganeloSearchPage>(this), create_search_bar("Search...", 400)}); - auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); - auto history_page = std::make_unique<HistoryPage>(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<HistoryPage>(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<MangaGenericSearchPage>(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<HistoryPage>(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<HistoryPage>(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<MangaGenericSearchPage>(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<HistoryPage>(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<HistoryPage>(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<MangaGenericSearchPage>(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<HistoryPage>(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<HistoryPage>(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<MangadexSearchPage>(this), create_search_bar("Search...", 400)}); - auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); - auto history_page = std::make_unique<HistoryPage>(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<HistoryPage>(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<MangaGenericSearchPage>(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<HistoryPage>(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<HistoryPage>(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<ManganeloSearchPage>(this); auto manganelos = std::make_unique<MangaGenericSearchPage>(this, "manganelos", "http://manganelos.com/"); @@ -1069,13 +917,11 @@ namespace QuickMedia { tabs.push_back(Tab{create_body(), std::make_unique<YoutubeSearchPage>(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<HistoryPage>(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<HistoryPage>(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<RecommendedPage>(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<YoutubeRecommendedPage>(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<MediaGenericSearchPage>(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<LazyFetchPage*>(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<ImageBoardThreadPage*>(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<VideoPage*>(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<void(const char*)> 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<VideoPlayer>(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<VideoPlayer>(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<const char*> 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<std::mutex> 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<CommandArg> get_cookies() { std::lock_guard<std::mutex> 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<CommandArg> 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<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; + + 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<Tab> &result_tabs) { + result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, url), nullptr}); + return PluginResult::OK; + } + + PluginResult YoutubeRecommendedPage::lazy_fetch(BodyItems &result_items) { + current_page = 0; + continuation_token.clear(); + added_videos.clear(); + + std::vector<CommandArg> additional_args = { + { "-H", "x-youtube-client-name: 1" }, + { "-H", "x-youtube-client-version: 2.20200626.03.00" } + }; + + 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, "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<Tab> &result_tabs) { result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(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; |