aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-08-15 20:46:59 +0200
committerdec05eba <dec05eba@protonmail.com>2020-08-15 20:53:44 +0200
commitb31bbdf6cf133f16752a7b0f78cbe9ae2fa667e2 (patch)
tree1b2c3a7b7cf02d3f48b64e8fb40f1f4e129592f0 /src
parent169057193c664f52267b818b2ff8bdcf73f21747 (diff)
Add youtube recommendations based on unwatched related videos
Diffstat (limited to 'src')
-rw-r--r--src/QuickMedia.cpp156
-rw-r--r--src/plugins/Mangadex.cpp2
-rw-r--r--src/plugins/Youtube.cpp5
3 files changed, 153 insertions, 10 deletions
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index d3627e1..c1886f3 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -397,6 +397,68 @@ namespace QuickMedia {
}
}
+ static void fill_recommended_items_from_json(const Json::Value &recommended_json, BodyItems &body_items) {
+ assert(recommended_json.isObject());
+
+ 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())
+ recommended_items.push_back(std::make_pair(member_name, std::move(recommended_item)));
+ }
+
+ /* 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 = std::make_unique<BodyItem>(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 + "/hqdefault.jpg";
+ body_items.push_back(std::move(body_item));
+ }
+ }
+
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) {
@@ -410,7 +472,21 @@ namespace QuickMedia {
return video_history_filepath.join(plugin->name).append(".json");
}
+ static Path get_recommended_filepath(Plugin *plugin) {
+ 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");
+ }
+
// 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_video_history_json(Plugin *plugin) {
Path video_history_filepath = get_video_history_filepath(plugin);
Json::Value json_result;
@@ -419,6 +495,16 @@ 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(Plugin *plugin) {
+ Path recommended_filepath = get_recommended_filepath(plugin);
+ 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::plugin_get_watch_history(Plugin *plugin, BodyItems &history_items) {
// TOOD: Make generic, instead of checking for plugin
if(plugin->is_manga()) {
@@ -506,10 +592,11 @@ namespace QuickMedia {
sf::Text history_tab_text("History", font, tab_text_size);
sf::Text recommended_tab_text("Recommended", font, tab_text_size);
- // if(current_plugin->name == "youtube") {
- // recommended_body = std::make_unique<Body>(this, font, bold_font);
- // recommended_body->draw_thumbnails = true;
- // }
+ if(current_plugin->name == "youtube") {
+ recommended_body = std::make_unique<Body>(this, font, bold_font);
+ recommended_body->draw_thumbnails = true;
+ fill_recommended_items_from_json(load_recommended_json(current_plugin), recommended_body->items);
+ }
struct Tab {
Body *body;
@@ -604,6 +691,7 @@ namespace QuickMedia {
};
std::future<BodyItems> recommended_future;
+ /*
if(recommended_body) {
recommended_future = std::async(std::launch::async, [this]() {
BodyItems body_items;
@@ -611,9 +699,10 @@ namespace QuickMedia {
return body_items;
});
} else {
+ */
PluginResult front_page_result = current_plugin->get_front_page(body->items);
body->clamp_selection();
- }
+ /*}*/
sf::Vector2f body_pos;
sf::Vector2f body_size;
@@ -697,11 +786,13 @@ namespace QuickMedia {
autocomplete_running = false;
}
+ /*
if(recommended_future.valid() && recommended_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
recommended_body->items = recommended_future.get();
recommended_body->filter_search_fuzzy(recommended_filter);
recommended_body->clamp_selection();
}
+ */
window.clear(back_color);
{
@@ -897,6 +988,8 @@ namespace QuickMedia {
show_notification("Video player", err_msg.c_str(), Urgency::CRITICAL);
current_page = previous_page;
} else {
+ related_media = current_plugin->get_related_media(content_url);
+
// TODO: Make this also work for other video plugins
if(current_plugin->name != "youtube")
return;
@@ -915,18 +1008,65 @@ namespace QuickMedia {
int existing_index = watch_history_get_item_by_id(video_history_json, video_id.c_str());
if(existing_index != -1) {
Json::Value removed;
+ /* TODO: Optimize. This is slow */
video_history_json.removeIndex(existing_index, &removed);
}
+ time_t time_now = time(NULL);
+
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);
+ new_content_object["timestamp"] = time_now;
- video_history_json.append(new_content_object);
+ video_history_json.append(std::move(new_content_object));
Path video_history_filepath = get_video_history_filepath(current_plugin);
save_json_to_file_atomic(video_history_filepath, video_history_json);
+
+ Json::Value recommended_json = load_recommended_json(current_plugin);
+ for(const auto &body_item : related_media) {
+ 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->title;
+ new_content_object["recommended_timestamp"] = time_now;
+ new_content_object["recommended_count"] = 1;
+ recommended_json[recommended_video_id] = std::move(new_content_object);
+ }
+ } else {
+ fprintf(stderr, "Failed to extract id of youtube url %s, video wont be saved in recommendations\n", content_url.c_str());
+ }
+ }
+
+ 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"] = content_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);
+ }
+
+ save_json_to_file_atomic(get_recommended_filepath(current_plugin), recommended_json);
}
};
@@ -948,8 +1088,8 @@ namespace QuickMedia {
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
+ // TODO: Remove duplicates
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;
diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp
index d2dee13..78c8ec9 100644
--- a/src/plugins/Mangadex.cpp
+++ b/src/plugins/Mangadex.cpp
@@ -61,6 +61,7 @@ namespace QuickMedia {
return SearchResult::ERR;
std::vector<std::pair<std::string, Json::Value>> chapters(chapter_json.size());
+ /* TODO: Optimize member access */
for(auto &member_name : chapter_json.getMemberNames()) {
Json::Value chapter = chapter_json[member_name];
if(chapter.isObject())
@@ -81,6 +82,7 @@ namespace QuickMedia {
time_t time_now = time(NULL);
+ /* TODO: Pointer */
std::string prev_chapter_number;
for(auto it = chapters.begin(); it != chapters.end(); ++it) {
const std::string &chapter_id = it->first;
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index b50939e..6f21991 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -62,10 +62,11 @@ namespace QuickMedia {
auto body_item = std::make_unique<BodyItem>(title);
/* TODO: Make date a different color */
- if(date) {
+ /* TODO: Do not append to title, messes up history.. */
+ /*if(date) {
body_item->title += '\n';
body_item->title += date;
- }
+ }*/
body_item->url = "https://www.youtube.com/watch?v=" + video_id_str;
body_item->thumbnail_url = std::move(thumbnail_url);
added_videos.insert(video_id_str);