aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-07-11 07:57:49 +0200
committerdec05eba <dec05eba@protonmail.com>2020-07-11 07:57:49 +0200
commit1be647662eb19af2e8fe27a500ac3705c3109045 (patch)
tree76f31687c378647439163f27e9d1b650307b28ae
parent2e156d80e4e3d379849bb1e127e4c69a8f34cea4 (diff)
Add youtube watch history
-rw-r--r--include/Path.hpp5
-rw-r--r--include/QuickMedia.hpp3
-rw-r--r--src/QuickMedia.cpp203
-rw-r--r--src/VideoPlayer.cpp2
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<std::string>(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::CharReader> 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 &timestamp = item["timestamp"];
+ //if(!timestamp.isNumeric())
+ // continue;
+
+ auto body_item = std::make_unique<BodyItem>(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<Tab, 2> 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<BodyItem>(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<Tab, 2> 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);