From de5fa7773f0393bfab917f8bf8606cb38ba3930e Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 18 Feb 2022 19:26:23 +0100 Subject: Refactor watch progress (prepare for global watch progress) --- TODO | 4 +- plugins/LocalAnime.hpp | 15 ++-- plugins/Page.hpp | 2 +- plugins/WatchProgress.hpp | 26 +++++++ src/plugins/LocalAnime.cpp | 160 ++++++++++-------------------------------- src/plugins/WatchProgress.cpp | 115 ++++++++++++++++++++++++++++++ 6 files changed, 188 insertions(+), 134 deletions(-) create mode 100644 plugins/WatchProgress.hpp create mode 100644 src/plugins/WatchProgress.cpp diff --git a/TODO b/TODO index d155689..4c2611d 100644 --- a/TODO +++ b/TODO @@ -219,4 +219,6 @@ Allow asynchronously loading body items. This is needed in manga combined plugin Bold video subtitles. Add ctrl+o keybind to open the selected media in an external application (video, imageviewer, music player should be configured in the config.json file). Open other files or youtube links in a browser when using this keybind. Local anime bookmark. -Local anime history. \ No newline at end of file +Local anime history. +Replace youtube subscriptions page (rss) with youtube api. This allows us to get get video status for videos (and description). +Add watch progress to youtube (and maybe also lbry and peertube?). \ No newline at end of file diff --git a/plugins/LocalAnime.hpp b/plugins/LocalAnime.hpp index 4c3efaa..7fe58d9 100644 --- a/plugins/LocalAnime.hpp +++ b/plugins/LocalAnime.hpp @@ -1,18 +1,11 @@ #pragma once #include "Page.hpp" +#include "WatchProgress.hpp" #include #include namespace QuickMedia { - struct LocalAnimeWatchProgress { - double time = 0.0; - double duration = 0.0; - - double get_watch_ratio() const; - bool has_finished_watching() const; - }; - struct LocalAnime; struct LocalAnimeSeason; struct LocalAnimeEpisode; @@ -59,14 +52,14 @@ namespace QuickMedia { class LocalAnimeVideoPage : public VideoPage { public: - LocalAnimeVideoPage(Program *program, std::string filepath, LocalAnimeWatchProgress watch_progress) + LocalAnimeVideoPage(Program *program, std::string filepath, WatchProgress watch_progress) : VideoPage(program, std::move(filepath)), watch_progress(std::move(watch_progress)) {} const char* get_title() const override { return ""; } std::string get_video_url(int max_height, bool &has_embedded_audio, std::string &ext) override; std::string get_url_timestamp() override; bool is_local() const override { return true; } - void set_watch_progress(double time_pos_sec) override; + void set_watch_progress(int64_t time_pos_sec) override; private: - LocalAnimeWatchProgress watch_progress; + WatchProgress watch_progress; }; } \ No newline at end of file diff --git a/plugins/Page.hpp b/plugins/Page.hpp index 2ab19b1..f5b4b3a 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -177,7 +177,7 @@ namespace QuickMedia { virtual bool is_local() const { return false; } - virtual void set_watch_progress(double time_pos_sec) { (void)time_pos_sec; } + virtual void set_watch_progress(int64_t time_pos_sec) { (void)time_pos_sec; } protected: std::string url; }; diff --git a/plugins/WatchProgress.hpp b/plugins/WatchProgress.hpp new file mode 100644 index 0000000..48f549c --- /dev/null +++ b/plugins/WatchProgress.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +namespace QuickMedia { + enum class WatchedStatus { + WATCHED, + NOT_WATCHED + }; + + struct WatchProgress { + int64_t time_pos_sec = 0; + int64_t duration_sec = 0; + time_t timestamp = 0; + std::string thumbnail_url; + + double get_watch_ratio() const; + bool has_finished_watching() const; + }; + + bool set_watch_progress_for_plugin(const char *plugin_name, const std::string &id, int64_t time_pos_sec, int64_t duration_sec, const std::string &thumbnail_url); + std::unordered_map get_watch_progress_for_plugin(const char *plugin_name); + bool toggle_watched_for_plugin_save_to_file(const char *plugin_name, const std::string &id, int64_t duration_sec, const std::string &thumbnail_url, WatchedStatus &watched_status); +} \ No newline at end of file diff --git a/src/plugins/LocalAnime.cpp b/src/plugins/LocalAnime.cpp index 9757be1..53b26f5 100644 --- a/src/plugins/LocalAnime.cpp +++ b/src/plugins/LocalAnime.cpp @@ -41,7 +41,7 @@ namespace QuickMedia { } LocalAnimeItem anime_item; - LocalAnimeWatchProgress watch_progress; + WatchProgress watch_progress; }; static std::vector get_episodes_in_directory(const Path &directory) { @@ -133,90 +133,6 @@ namespace QuickMedia { } } - static LocalAnimeWatchProgress get_watch_progress(const Json::Value &watched_json, const LocalAnimeItem &item) { - LocalAnimeWatchProgress progress; - Path latest_anime_path = get_latest_anime_item(item)->path; - - std::string filename_relative_to_anime_dir = latest_anime_path.data.substr(get_config().local_anime.directory.size() + 1); - const Json::Value *found_watched_item = watched_json.find( - filename_relative_to_anime_dir.data(), - filename_relative_to_anime_dir.data() + filename_relative_to_anime_dir.size()); - if(!found_watched_item || !found_watched_item->isObject()) - return progress; - - const Json::Value &time_json = (*found_watched_item)["time"]; - const Json::Value &duration_json = (*found_watched_item)["duration"]; - if(!time_json.isInt64() || !duration_json.isInt64() || duration_json.asInt64() == 0) - return progress; - - // We consider having watched the anime if the user stopped watching 90% in, because they might skip the ending theme/credits - progress.time = (double)time_json.asInt64(); - progress.duration = (double)duration_json.asInt64(); - return progress; - } - - enum class WatchedStatus { - WATCHED, - NOT_WATCHED - }; - - static bool toggle_watched_save_to_file(const Path &filepath, WatchedStatus &watched_status) { - Path local_anime_progress_path = get_storage_dir().join("watch-progress").join("local-anime"); - - Json::Value json_root; - if(!read_file_as_json(local_anime_progress_path, json_root) || !json_root.isObject()) - json_root = Json::Value(Json::objectValue); - - bool watched = false; - std::string filename_relative_to_anime_dir = filepath.data.substr(get_config().local_anime.directory.size() + 1); - Json::Value &watched_item = json_root[filename_relative_to_anime_dir]; - if(watched_item.isObject()) { - watched = true; - } else { - watched_item = Json::Value(Json::objectValue); - watched = false; - } - - if(watched) { - json_root.removeMember(filename_relative_to_anime_dir.c_str()); - } else { - FileAnalyzer file_analyzer; - if(!file_analyzer.load_file(filepath.data.c_str(), true)) { - show_notification("QuickMedia", "Failed to mark " + filename_relative_to_anime_dir + " as watched", Urgency::CRITICAL); - return false; - } - - if(!file_analyzer.get_duration_seconds() || *file_analyzer.get_duration_seconds() == 0) { - show_notification("QuickMedia", "Failed to mark " + filename_relative_to_anime_dir + " as watched", Urgency::CRITICAL); - return false; - } - - watched_item["time"] = (int64_t)*file_analyzer.get_duration_seconds(); - watched_item["duration"] = (int64_t)*file_analyzer.get_duration_seconds(); - watched_item["thumbnail_url"] = filename_relative_to_anime_dir; - watched_item["timestamp"] = (int64_t)time(nullptr); - } - - if(!save_json_to_file_atomic(local_anime_progress_path, json_root)) { - show_notification("QuickMedia", "Failed to mark " + filename_relative_to_anime_dir + " as " + (watched ? "not watched" : "watched"), Urgency::CRITICAL); - return false; - } - - watched_status = watched ? WatchedStatus::NOT_WATCHED : WatchedStatus::WATCHED; - return true; - } - - double LocalAnimeWatchProgress::get_watch_ratio() const { - if(duration == 0.0) - return 0.0; - return (double)time / (double)duration; - } - - // We consider having watched the anime if the user stopped watching 90% in, because they might skip the ending theme/credits - bool LocalAnimeWatchProgress::has_finished_watching() const { - return get_watch_ratio() >= 0.9; - } - PluginResult LocalAnimeSearchPage::submit(const SubmitArgs &args, std::vector &result_tabs) { LocalAnimeBodyItemData *item_data = static_cast(args.extra.get()); if(std::holds_alternative(item_data->anime_item)) { @@ -236,9 +152,7 @@ namespace QuickMedia { } PluginResult LocalAnimeSearchPage::lazy_fetch(BodyItems &result_items) { - Json::Value json_root; - if(!read_file_as_json(get_storage_dir().join("watch-progress").join("local-anime"), json_root) || !json_root.isObject()) - json_root = Json::Value(Json::objectValue); + std::unordered_map watch_progress = get_watch_progress_for_plugin("local-anime"); std::vector anime_items; switch(type) { @@ -253,12 +167,21 @@ namespace QuickMedia { break; } + fprintf(stderr, "num watched animu: %zu\n", watch_progress.size()); + for(auto &it : watch_progress) { + fprintf(stderr, "watch progress: %s, time: %d, duration: %d\n", it.first.c_str(), (int)it.second.time_pos_sec, (int)it.second.duration_sec); + } + const time_t time_now = time(nullptr); for(LocalAnimeItem &anime_item : anime_items) { + std::string filename_relative_to_anime_dir = get_latest_anime_item(anime_item)->path.data.substr(get_config().local_anime.directory.size() + 1); + auto body_item_data = std::make_shared(); - body_item_data->watch_progress = get_watch_progress(json_root, anime_item); + body_item_data->watch_progress = watch_progress[filename_relative_to_anime_dir]; const bool has_finished_watching = body_item_data->watch_progress.has_finished_watching(); + fprintf(stderr, "watch progress %s: time: %d, duration: %d\n", filename_relative_to_anime_dir.c_str(), (int)body_item_data->watch_progress.time_pos_sec, (int)body_item_data->watch_progress.duration_sec); + if(std::holds_alternative(anime_item)) { const LocalAnime &anime = std::get(anime_item); @@ -332,8 +255,23 @@ namespace QuickMedia { return; LocalAnimeBodyItemData *item_data = static_cast(selected_item->extra.get()); + + Path latest_anime_path = get_latest_anime_item(item_data->anime_item)->path; + std::string filename_relative_to_anime_dir = latest_anime_path.data.substr(get_config().local_anime.directory.size() + 1); + + FileAnalyzer file_analyzer; + if(!file_analyzer.load_file(latest_anime_path.data.c_str(), true)) { + show_notification("QuickMedia", "Failed to load " + filename_relative_to_anime_dir + " to set watch progress", Urgency::CRITICAL); + return; + } + + if(!file_analyzer.get_duration_seconds()) { + show_notification("QuickMedia", "Failed to get duration of " + filename_relative_to_anime_dir + " to set watch progress", Urgency::CRITICAL); + return; + } + WatchedStatus watch_status; - if(!toggle_watched_save_to_file(get_latest_anime_item(item_data->anime_item)->path, watch_status)) + if(!toggle_watched_for_plugin_save_to_file("local-anime", filename_relative_to_anime_dir, *file_analyzer.get_duration_seconds(), latest_anime_path.data, watch_status)) return; mgl::Color color = get_theme().text_color; @@ -341,6 +279,12 @@ namespace QuickMedia { if(watch_status == WatchedStatus::WATCHED) { title = "[Finished watching] "; color = finished_watching_color; + + item_data->watch_progress.time_pos_sec = *file_analyzer.get_duration_seconds(); + item_data->watch_progress.duration_sec = *file_analyzer.get_duration_seconds(); + } else { + item_data->watch_progress.time_pos_sec = 0; + item_data->watch_progress.duration_sec = *file_analyzer.get_duration_seconds(); } title += Path(selected_item->url).filename(); @@ -358,13 +302,13 @@ namespace QuickMedia { // If we are very close to the end then start from the beginning. // This is the same behavior as mpv. // This is better because we dont want the video player to stop immediately after we start playing and we dont get any chance to seek. - if(watch_progress.time + 10.0 >= watch_progress.duration) + if(watch_progress.time_pos_sec + 10.0 >= watch_progress.duration_sec) return "0"; else - return std::to_string(watch_progress.time); + return std::to_string(watch_progress.time_pos_sec); } - void LocalAnimeVideoPage::set_watch_progress(double time_pos_sec) { + void LocalAnimeVideoPage::set_watch_progress(int64_t time_pos_sec) { std::string filename_relative_to_anime_dir = url.substr(get_config().local_anime.directory.size() + 1); FileAnalyzer file_analyzer; @@ -378,33 +322,7 @@ namespace QuickMedia { return; } - watch_progress.duration = *file_analyzer.get_duration_seconds(); - - Path watch_progress_dir = get_storage_dir().join("watch-progress"); - if(create_directory_recursive(watch_progress_dir) != 0) { - show_notification("QuickMedia", "Failed to create " + watch_progress_dir.data + " to set watch progress for " + filename_relative_to_anime_dir, Urgency::CRITICAL); - return; - } - - Path local_anime_progress_path = watch_progress_dir; - local_anime_progress_path.join("local-anime"); - - Json::Value json_root; - if(!read_file_as_json(local_anime_progress_path, json_root) || !json_root.isObject()) - json_root = Json::Value(Json::objectValue); - - Json::Value watch_progress_json(Json::objectValue); - watch_progress_json["time"] = (int64_t)time_pos_sec; - watch_progress_json["duration"] = (int64_t)watch_progress.duration; - watch_progress_json["thumbnail_url"] = filename_relative_to_anime_dir; - watch_progress_json["timestamp"] = (int64_t)time(nullptr); - json_root[filename_relative_to_anime_dir] = std::move(watch_progress_json); - - if(!save_json_to_file_atomic(local_anime_progress_path, json_root)) { - show_notification("QuickMedia", "Failed to set watch progress for " + filename_relative_to_anime_dir, Urgency::CRITICAL); - return; - } - - fprintf(stderr, "Set watch progress for \"%s\" to %d/%d\n", filename_relative_to_anime_dir.c_str(), (int)time_pos_sec, (int)watch_progress.duration); + watch_progress.duration_sec = *file_analyzer.get_duration_seconds(); + set_watch_progress_for_plugin("local-anime", filename_relative_to_anime_dir, time_pos_sec, *file_analyzer.get_duration_seconds(), url); } } \ No newline at end of file diff --git a/src/plugins/WatchProgress.cpp b/src/plugins/WatchProgress.cpp new file mode 100644 index 0000000..c3e2fb7 --- /dev/null +++ b/src/plugins/WatchProgress.cpp @@ -0,0 +1,115 @@ +#include "../../plugins/WatchProgress.hpp" +#include "../../include/Storage.hpp" +#include "../../include/Notification.hpp" +#include + +namespace QuickMedia { + double WatchProgress::get_watch_ratio() const { + if(duration_sec == 0) + return 0; + return (double)time_pos_sec / (double)duration_sec; + } + + // We consider having watched the video if the user stopped watching 90% in, because they might skip the ending theme/credits (especially in anime) + bool WatchProgress::has_finished_watching() const { + return get_watch_ratio() >= 0.9; + } + + bool set_watch_progress_for_plugin(const char *plugin_name, const std::string &id, int64_t time_pos_sec, int64_t duration_sec, const std::string &thumbnail_url) { + Path watch_progress_dir = get_storage_dir().join("watch-progress"); + if(create_directory_recursive(watch_progress_dir) != 0) { + show_notification("QuickMedia", "Failed to create " + watch_progress_dir.data + " to set watch progress for " + id, Urgency::CRITICAL); + return false; + } + + Path progress_path = watch_progress_dir; + progress_path.join(plugin_name); + + Json::Value json_root; + if(!read_file_as_json(progress_path, json_root) || !json_root.isObject()) + json_root = Json::Value(Json::objectValue); + + Json::Value watch_progress_json(Json::objectValue); + watch_progress_json["time"] = (int64_t)time_pos_sec; + watch_progress_json["duration"] = (int64_t)duration_sec; + watch_progress_json["thumbnail_url"] = thumbnail_url; + watch_progress_json["timestamp"] = (int64_t)time(nullptr); + json_root[id] = std::move(watch_progress_json); + + if(!save_json_to_file_atomic(progress_path, json_root)) { + show_notification("QuickMedia", "Failed to set watch progress for " + id, Urgency::CRITICAL); + return false; + } + + fprintf(stderr, "Set watch progress for \"%s\" to %d/%d\n", id.c_str(), (int)time_pos_sec, (int)duration_sec); + return true; + } + + std::unordered_map get_watch_progress_for_plugin(const char *plugin_name) { + std::unordered_map watch_progress_map; + Path progress_path = get_storage_dir().join("watch-progress").join(plugin_name); + + Json::Value json_root; + if(!read_file_as_json(progress_path, json_root) || !json_root.isObject()) + return watch_progress_map; + + for(Json::Value::const_iterator it = json_root.begin(); it != json_root.end(); ++it) { + Json::Value key = it.key(); + if(!key.isString()) + continue; + + const Json::Value &time_json = (*it)["time"]; + const Json::Value &duration_json = (*it)["duration"]; + const Json::Value ×tamp_json = (*it)["timestamp"]; + if(!time_json.isInt64() || !duration_json.isInt64() || !timestamp_json.isInt64()) + continue; + + WatchProgress watch_progress; + watch_progress.time_pos_sec = time_json.asInt64(); + watch_progress.duration_sec = duration_json.asInt64(); + watch_progress.timestamp = timestamp_json.asInt64(); + + const Json::Value &thumbnail_url_json = (*it)["thumbnail_url"]; + if(thumbnail_url_json.isString()) + watch_progress.thumbnail_url = thumbnail_url_json.asString(); + + watch_progress_map[key.asString()] = std::move(watch_progress); + } + + return watch_progress_map; + } + + bool toggle_watched_for_plugin_save_to_file(const char *plugin_name, const std::string &id, int64_t duration_sec, const std::string &thumbnail_url, WatchedStatus &watched_status) { + Path local_anime_progress_path = get_storage_dir().join("watch-progress").join(plugin_name); + + Json::Value json_root; + if(!read_file_as_json(local_anime_progress_path, json_root) || !json_root.isObject()) + json_root = Json::Value(Json::objectValue); + + bool watched = false; + Json::Value &watched_item = json_root[id]; + if(watched_item.isObject()) { + watched = true; + } else { + watched_item = Json::Value(Json::objectValue); + watched = false; + } + + if(watched) { + json_root.removeMember(id.c_str()); + } else { + watched_item["time"] = (int64_t)duration_sec; + watched_item["duration"] = (int64_t)duration_sec; + watched_item["thumbnail_url"] = thumbnail_url; + watched_item["timestamp"] = (int64_t)time(nullptr); + } + + if(!save_json_to_file_atomic(local_anime_progress_path, json_root)) { + show_notification("QuickMedia", "Failed to mark " + id + " as " + (watched ? "not watched" : "watched"), Urgency::CRITICAL); + return false; + } + + watched_status = watched ? WatchedStatus::NOT_WATCHED : WatchedStatus::WATCHED; + return true; + } +} \ No newline at end of file -- cgit v1.2.3