From 1be647662eb19af2e8fe27a500ac3705c3109045 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 11 Jul 2020 07:57:49 +0200 Subject: Add youtube watch history --- include/Path.hpp | 5 ++ include/QuickMedia.hpp | 3 + src/QuickMedia.cpp | 203 +++++++++++++++++++++++++++++++++++++++++-------- src/VideoPlayer.cpp | 2 +- 4 files changed, 180 insertions(+), 33 deletions(-) diff --git a/include/Path.hpp b/include/Path.hpp index 351fd5d..bd978bc 100644 --- a/include/Path.hpp +++ b/include/Path.hpp @@ -21,6 +21,11 @@ namespace QuickMedia { return *this; } + Path& append(const std::string &str) { + data += str; + return *this; + } + std::string data; }; } \ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 584903a..9bf2d0f 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -56,6 +56,9 @@ namespace QuickMedia { // Returns Page::EXIT if empty Page pop_page_stack(); + + void plugin_get_watch_history(Plugin *plugin, BodyItems &history_items); + Json::Value load_video_history_json(Plugin *plugin); private: Display *disp; sf::RenderWindow window; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index df5a1e8..32913a3 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -319,58 +319,113 @@ namespace QuickMedia { return cppcodec::base64_rfc4648::decode(data); } - static bool read_file_as_json(const Path &storage_path, Json::Value &result) { + static bool read_file_as_json(const Path &filepath, Json::Value &result) { std::string file_content; - if(file_get_content(storage_path, file_content) != 0) + if(file_get_content(filepath, file_content) != 0) { + fprintf(stderr, "Failed to get content of file: %s\n", filepath.data.c_str()); return false; + } Json::CharReaderBuilder json_builder; std::unique_ptr json_reader(json_builder.newCharReader()); std::string json_errors; if(!json_reader->parse(file_content.data(), file_content.data() + file_content.size(), &result, &json_errors)) { - fprintf(stderr, "Failed to read json, error: %s\n", json_errors.c_str()); + fprintf(stderr, "Failed to read file %s as json, error: %s\n", filepath.data.c_str(), json_errors.c_str()); return false; } return true; } - static bool save_manga_progress_json(const Path &path, const Json::Value &json) { + static bool save_json_to_file(const Path &path, const Json::Value &json) { Json::StreamWriterBuilder json_builder; return file_overwrite(path, Json::writeString(json_builder, json)) == 0; } + static bool save_json_to_file_atomic(const Path &path, const Json::Value &json) { + Path tmp_path = path; + tmp_path.append(".tmp"); + + Json::StreamWriterBuilder json_builder; + if(file_overwrite(tmp_path, Json::writeString(json_builder, json)) != 0) + return false; + + // Rename is atomic under posix! + if(rename(tmp_path.data.c_str(), path.data.c_str()) != 0) { + perror("save_json_to_file_atomic rename"); + return false; + } + + return true; + } + enum class SearchSuggestionTab { ALL, HISTORY }; - void Program::search_suggestion_page() { - std::string update_search_text; - bool search_running = false; - bool typing = false; + static void fill_history_items_from_json(const Json::Value &history_json, BodyItems &history_items) { + assert(history_json.isArray()); - std::string autocomplete_text; - bool autocomplete_running = false; + if(history_json.empty()) + return; - Body history_body(this, font, bold_font); - const float tab_text_size = 18.0f; - const float tab_height = tab_text_size + 10.0f; - sf::Text all_tab_text("All", font, tab_text_size); - sf::Text history_tab_text("History", font, tab_text_size); + auto begin = history_json.begin(); + --begin; + auto end = history_json.end(); + while(end != begin) { + --end; + const Json::Value &item = *end; + if(!item.isObject()) + continue; + + const Json::Value &video_id = item["id"]; + if(!video_id.isString()) + continue; + std::string video_id_str = video_id.asString(); + + const Json::Value &title = item["title"]; + if(!title.isString()) + continue; + std::string title_str = title.asString(); + + //const Json::Value ×tamp = item["timestamp"]; + //if(!timestamp.isNumeric()) + // continue; + + auto body_item = std::make_unique(std::move(title_str)); + body_item->url = "https://youtube.com/watch?v=" + video_id_str; + body_item->thumbnail_url = "https://img.youtube.com/vi/" + video_id_str + "/hqdefault.jpg"; + history_items.push_back(std::move(body_item)); + } + } - struct Tab { - Body *body; - SearchSuggestionTab tab; - sf::Text *text; - }; + static Path get_video_history_filepath(Plugin *plugin) { + Path video_history_dir = get_storage_dir().join("history"); + if(create_directory_recursive(video_history_dir) != 0) { + std::string err_msg = "Failed to create video history directory "; + err_msg += video_history_dir.data; + show_notification("Video player", err_msg.c_str(), Urgency::CRITICAL); + exit(1); + } - std::array tabs = { Tab{body, SearchSuggestionTab::ALL, &all_tab_text}, Tab{&history_body, SearchSuggestionTab::HISTORY, &history_tab_text} }; - int selected_tab = 0; + Path video_history_filepath = video_history_dir; + return video_history_filepath.join(plugin->name).append(".json"); + } + + // This is not cached because we could have multiple instances of QuickMedia running the same plugin! + Json::Value Program::load_video_history_json(Plugin *plugin) { + Path video_history_filepath = get_video_history_filepath(plugin); + Json::Value json_result; + if(!read_file_as_json(video_history_filepath, json_result) || !json_result.isArray()) + json_result = Json::Value(Json::arrayValue); + return json_result; + } + void Program::plugin_get_watch_history(Plugin *plugin, BodyItems &history_items) { // TOOD: Make generic, instead of checking for plugin - if(current_plugin->is_manga()) { - Path content_storage_dir = get_storage_dir().join(current_plugin->name); + if(plugin->is_manga()) { + Path content_storage_dir = get_storage_dir().join(plugin->name); if(create_directory_recursive(content_storage_dir) != 0) { show_notification("Storage", "Failed to create directory: " + content_storage_dir.data, Urgency::CRITICAL); exit(1); @@ -381,7 +436,7 @@ namespace QuickMedia { exit(1); } // TODO: Make asynchronous - for_files_in_dir_sort_last_modified(content_storage_dir, [&history_body, this](const std::filesystem::path &filepath) { + for_files_in_dir_sort_last_modified(content_storage_dir, [&history_items, plugin](const std::filesystem::path &filepath) { Path fullpath(filepath.c_str()); Json::Value body; if(!read_file_as_json(fullpath, body)) { @@ -394,20 +449,54 @@ namespace QuickMedia { if(!filename.empty() && manga_name.isString()) { // TODO: Add thumbnail auto body_item = std::make_unique(manga_name.asString()); - if(current_plugin->name == "manganelo") + if(plugin->name == "manganelo") body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string()); - else if(current_plugin->name == "mangadex") + else if(plugin->name == "mangadex") body_item->url = "https://mangadex.org/title/" + base64_decode(filename.string()); - else if(current_plugin->name == "mangatown") + else if(plugin->name == "mangatown") body_item->url = "https://mangatown.com/manga/" + base64_decode(filename.string()); else fprintf(stderr, "Error: Not implemented: filename to manga chapter list\n"); - history_body.items.push_back(std::move(body_item)); + history_items.push_back(std::move(body_item)); } return true; }); + return; } + if(plugin->name != "youtube") + return; + + fill_history_items_from_json(load_video_history_json(plugin), history_items); + } + + void Program::search_suggestion_page() { + std::string update_search_text; + bool search_running = false; + bool typing = false; + + std::string autocomplete_text; + bool autocomplete_running = false; + + Body history_body(this, font, bold_font); + const float tab_text_size = 18.0f; + const float tab_height = tab_text_size + 10.0f; + sf::Text all_tab_text("All", font, tab_text_size); + sf::Text history_tab_text("History", font, tab_text_size); + + struct Tab { + Body *body; + SearchSuggestionTab tab; + sf::Text *text; + }; + + std::array tabs = { Tab{body, SearchSuggestionTab::ALL, &all_tab_text}, Tab{&history_body, SearchSuggestionTab::HISTORY, &history_tab_text} }; + int selected_tab = 0; + + plugin_get_watch_history(current_plugin, history_body.items); + if(current_plugin->name == "youtube") + history_body.draw_thumbnails = true; + search_bar->onTextBeginTypingCallback = [&typing]() { typing = true; }; @@ -692,6 +781,25 @@ namespace QuickMedia { return false; } + + static bool watch_history_contains_id(const Json::Value &video_history_json, const char *id) { + assert(video_history_json.isArray()); + + for(const Json::Value &item : video_history_json) { + if(!item.isObject()) + continue; + + const Json::Value &id_json = item["id"]; + if(!id_json.isString()) + continue; + + if(strcmp(id, id_json.asCString()) == 0) + return true; + } + + return false; + } + void Program::video_content_page() { search_bar->onTextUpdateCallback = nullptr; search_bar->onTextSubmitCallback = nullptr; @@ -715,6 +823,34 @@ namespace QuickMedia { err_msg += content_url; show_notification("Video player", err_msg.c_str(), Urgency::CRITICAL); current_page = previous_page; + } else { + // TODO: Make this also work for other video plugins + if(current_plugin->name != "youtube") + return; + + std::string video_id; + if(!youtube_url_extract_id(content_url, video_id)) { + std::string err_msg = "Failed to extract id of youtube url "; + err_msg += content_url; + err_msg + ", video wont be saved in history"; + show_notification("Video player", err_msg.c_str(), Urgency::LOW); + return; + } + + Json::Value video_history_json = load_video_history_json(current_plugin); + + if(watch_history_contains_id(video_history_json, video_id.c_str())) + return; + + Json::Value new_content_object(Json::objectValue); + new_content_object["id"] = video_id; + new_content_object["title"] = content_title; + new_content_object["timestamp"] = time(NULL); + + video_history_json.append(new_content_object); + + Path video_history_filepath = get_video_history_filepath(current_plugin); + save_json_to_file_atomic(video_history_filepath, video_history_json); } }; @@ -734,11 +870,13 @@ namespace QuickMedia { if(end_of_file && has_video_started) { has_video_started = false; std::string new_video_url; + std::string new_video_title; BodyItems related_media = current_plugin->get_related_media(content_url); // Find video that hasn't been played before in this video session for(auto it = related_media.begin(), end = related_media.end(); it != end; ++it) { if(watched_videos.find((*it)->url) == watched_videos.end()) { new_video_url = (*it)->url; + new_video_title = (*it)->title; break; } } @@ -751,6 +889,7 @@ namespace QuickMedia { } content_url = std::move(new_video_url); + content_title = std::move(new_video_title); load_video_error_check(); } }; @@ -1119,7 +1258,7 @@ namespace QuickMedia { json_chapter["current"] = std::min(latest_read, num_images); json_chapter["total"] = num_images; json_chapters[chapter_title] = json_chapter; - if(!save_manga_progress_json(content_storage_file, content_storage_json)) { + if(!save_json_to_file(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } @@ -1285,7 +1424,7 @@ namespace QuickMedia { json_chapter["current"] = std::min(latest_read, image_viewer.get_num_pages()); json_chapter["total"] = image_viewer.get_num_pages(); json_chapters[chapter_title] = json_chapter; - if(!save_manga_progress_json(content_storage_file, content_storage_json)) { + if(!save_json_to_file(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } @@ -1311,7 +1450,7 @@ namespace QuickMedia { latest_read = focused_page; json_chapter["current"] = latest_read; json_chapters[chapter_title] = json_chapter; - if(!save_manga_progress_json(content_storage_file, content_storage_json)) { + if(!save_json_to_file(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } } diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp index 6024ca8..c30f147 100644 --- a/src/VideoPlayer.cpp +++ b/src/VideoPlayer.cpp @@ -113,7 +113,7 @@ namespace QuickMedia { VideoPlayer::Error VideoPlayer::load_video(const char *path, sf::WindowHandle _parent_window) { // This check is to make sure we dont change window that the video belongs to. This is not a usecase we will have so - // no need to support it for not at least. + // no need to support it for now at least. assert(parent_window == 0 || parent_window == _parent_window); if(video_process_id == -1) return launch_video_process(path, _parent_window); -- cgit v1.2.3