aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO4
-rw-r--r--plugins/LocalAnime.hpp15
-rw-r--r--plugins/Page.hpp2
-rw-r--r--plugins/WatchProgress.hpp26
-rw-r--r--src/plugins/LocalAnime.cpp160
-rw-r--r--src/plugins/WatchProgress.cpp115
6 files changed, 188 insertions, 134 deletions
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 <vector>
#include <variant>
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 <stdint.h>
+#include <string>
+#include <unordered_map>
+
+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<std::string, WatchProgress> 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<LocalAnimeItem> 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<Tab> &result_tabs) {
LocalAnimeBodyItemData *item_data = static_cast<LocalAnimeBodyItemData*>(args.extra.get());
if(std::holds_alternative<LocalAnime>(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<std::string, WatchProgress> watch_progress = get_watch_progress_for_plugin("local-anime");
std::vector<LocalAnimeItem> 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<LocalAnimeBodyItemData>();
- 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<LocalAnime>(anime_item)) {
const LocalAnime &anime = std::get<LocalAnime>(anime_item);
@@ -332,8 +255,23 @@ namespace QuickMedia {
return;
LocalAnimeBodyItemData *item_data = static_cast<LocalAnimeBodyItemData*>(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 <json/value.h>
+
+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<std::string, WatchProgress> get_watch_progress_for_plugin(const char *plugin_name) {
+ std::unordered_map<std::string, WatchProgress> 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 &timestamp_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