diff options
author | dec05eba <dec05eba@protonmail.com> | 2021-04-28 05:37:15 +0200 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2021-04-28 05:37:42 +0200 |
commit | 1b7abc7e819d055b3d0ea5be8967a1e381bb5d60 (patch) | |
tree | 503e6f67d933119df4512ef7186ed380b393f868 /src | |
parent | f045e579e1faa186ca0ebf6e6d1e562fbcd75727 (diff) |
Add youtube subscriptions, etc
Diffstat (limited to 'src')
-rw-r--r-- | src/Body.cpp | 5 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 14 | ||||
-rw-r--r-- | src/StringUtils.cpp | 11 | ||||
-rw-r--r-- | src/plugins/Manga.cpp | 8 | ||||
-rw-r--r-- | src/plugins/Matrix.cpp | 5 | ||||
-rw-r--r-- | src/plugins/Soundcloud.cpp | 10 | ||||
-rw-r--r-- | src/plugins/Youtube.cpp | 256 |
7 files changed, 285 insertions, 24 deletions
diff --git a/src/Body.cpp b/src/Body.cpp index a84256e..aa8d1d6 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -844,7 +844,8 @@ namespace QuickMedia { //struct tm *now_tm = localtime(&time_now); time_t message_timestamp = body_item->get_timestamp() / 1000; - struct tm *message_tm = localtime(&message_timestamp); + struct tm message_tm; + localtime_r(&message_timestamp, &message_tm); //bool is_same_year = message_tm->tm_year == now_tm->tm_year; @@ -855,7 +856,7 @@ namespace QuickMedia { else strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S %Y", message_tm); */ - strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M", message_tm); + strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M", &message_tm); if(body_item->timestamp_text) body_item->timestamp_text->setString(time_str); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 16f19d4..456bdc6 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -918,6 +918,8 @@ namespace QuickMedia { auto recommended_body = create_body(); fill_recommended_items_from_json(plugin_name, load_recommended_json(), recommended_body->items); tabs.push_back(Tab{std::move(recommended_body), std::make_unique<RecommendedPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + tabs.push_back(Tab{create_body(), std::make_unique<YoutubeSubscriptionsPage>(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "pornhub") == 0) { auto search_body = create_body(); tabs.push_back(Tab{std::move(search_body), std::make_unique<PornhubSearchPage>(this), create_search_bar("Search...", 500)}); @@ -1066,7 +1068,7 @@ namespace QuickMedia { auto body_item = BodyItem::create(std::move(title_str)); body_item->url = "https://www.youtube.com/watch?v=" + video_id_str; body_item->thumbnail_url = "https://img.youtube.com/vi/" + video_id_str + "/hqdefault.jpg"; - body_item->set_description(timestamp_to_relative_time_str(std::max(0l, time_now - timestamp.asInt64()))); + body_item->set_description("Watched " + timestamp_to_relative_time_str(std::max(0l, time_now - timestamp.asInt64()))); body_item->thumbnail_size = sf::Vector2i(175, 131); body_items.push_back(std::move(body_item)); } @@ -1595,14 +1597,8 @@ namespace QuickMedia { } else if(event.key.code == sf::Keyboard::T && event.key.control) { BodyItem *selected_item = tabs[selected_tab].body->get_selected(); if(selected_item && tabs[selected_tab].page && tabs[selected_tab].page->is_trackable()) { - TrackablePage *trackable_page = static_cast<TrackablePage*>(tabs[selected_tab].page.get()); - TrackResult track_result = trackable_page->track(selected_item->get_title()); - // TODO: Show proper error message when this fails. For example if we are already tracking the manga - if(track_result == TrackResult::OK) { - show_notification("QuickMedia", "You are now tracking \"" + trackable_page->content_title + "\" after \"" + selected_item->get_title() + "\"", Urgency::LOW); - } else { - show_notification("QuickMedia", "Failed to track media \"" + trackable_page->content_title + "\", chapter: \"" + selected_item->get_title() + "\"", Urgency::CRITICAL); - } + TrackablePage *trackable_page = dynamic_cast<TrackablePage*>(tabs[selected_tab].page.get()); + trackable_page->track(selected_item->get_title()); } } } diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index 56d746c..84e2ce5 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -16,6 +16,17 @@ namespace QuickMedia { } } + size_t string_replace_all(std::string &str, char old_char, char new_char) { + size_t num_replaced_substrings = 0; + for(char &c : str) { + if(c == old_char) { + c = new_char; + ++num_replaced_substrings; + } + } + return num_replaced_substrings; + } + size_t string_replace_all(std::string &str, char old_char, const std::string &new_str) { size_t num_replaced_substrings = 0; size_t index = 0; diff --git a/src/plugins/Manga.cpp b/src/plugins/Manga.cpp index f3c6814..2c7bae0 100644 --- a/src/plugins/Manga.cpp +++ b/src/plugins/Manga.cpp @@ -1,13 +1,17 @@ #include "../../plugins/Manga.hpp" #include "../../include/Program.hpp" +#include "../../include/Notification.hpp" namespace QuickMedia { TrackResult MangaChaptersPage::track(const std::string &str) { const char *args[] = { "automedia", "add", "html", content_url.data(), "--start-after", str.data(), "--name", content_title.data(), nullptr }; - if(exec_program(args, nullptr, nullptr) == 0) + if(exec_program(args, nullptr, nullptr) == 0) { + show_notification("QuickMedia", "You are now tracking \"" + content_title + "\" after \"" + str + "\"", Urgency::LOW); return TrackResult::OK; - else + } else { + show_notification("QuickMedia", "Failed to track media \"" + content_title + "\", chapter: \"" + str + "\"", Urgency::CRITICAL); return TrackResult::ERR; + } } void MangaChaptersPage::on_navigate_to_page(Body *body) { diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 3c4a6dd..4a414da 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -64,7 +64,10 @@ namespace QuickMedia { std::string extract_first_line_remove_newline_elipses(const std::string &str, size_t max_length) { std::string result = str; - string_replace_all(result, '\n', " "); + string_replace_all(result, '\n', ' '); + string_replace_all(result, '\t', ' '); + string_replace_all(result, '\v', ' '); + string_replace_all(result, '\r', ' '); size_t index = result.find('\n'); if(index == std::string::npos) { if(result.size() > max_length) diff --git a/src/plugins/Soundcloud.cpp b/src/plugins/Soundcloud.cpp index 90fe144..e0a4d24 100644 --- a/src/plugins/Soundcloud.cpp +++ b/src/plugins/Soundcloud.cpp @@ -299,14 +299,14 @@ namespace QuickMedia { if(result != 0) return PluginResult::ERR; - async_download_threads.clear(); + async_download_tasks.clear(); for(std::string &script_source : script_sources) { if(string_starts_with(script_source, "//")) script_source = "https://" + script_source.substr(2); else if(string_starts_with(script_source, "/")) script_source = "https://soundcloud.com/" + script_source.substr(1); - async_download_threads.push_back(AsyncTask<std::string>([script_source]() -> std::string { + async_download_tasks.push_back(AsyncTask<std::string>([script_source]() -> std::string { std::string website_data; DownloadResult download_result = download_to_string(script_source, website_data, {}, true); if(download_result != DownloadResult::OK) return ""; @@ -321,9 +321,9 @@ namespace QuickMedia { })); } - for(auto &download_thread : async_download_threads) { - if(download_thread.valid()) { - std::string fetched_client_id = download_thread.get(); + for(auto &download_task : async_download_tasks) { + if(download_task.valid()) { + std::string fetched_client_id = download_task.get(); if(client_id.empty() && !fetched_client_id.empty()) client_id = std::move(fetched_client_id); } diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index f7d38aa..9e07400 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -3,6 +3,10 @@ #include "../../include/NetUtils.hpp" #include "../../include/StringUtils.hpp" #include "../../include/Scale.hpp" +#include "../../include/Notification.hpp" +extern "C" { +#include <HtmlParser.h> +} #include <json/writer.h> #include <string.h> #include <unistd.h> @@ -138,9 +142,10 @@ namespace QuickMedia { return nullptr; time_t start_time = strtol(start_time_json.asCString(), nullptr, 10); - struct tm *message_tm = localtime(&start_time); + struct tm message_tm; + localtime_r(&start_time, &message_tm); char time_str[128] = {0}; - strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M", message_tm); + strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M", &message_tm); string_replace_all(upcoming_event_text.value(), "DATE_PLACEHOLDER", time_str); scheduled_text = std::move(upcoming_event_text.value()); } @@ -344,8 +349,8 @@ namespace QuickMedia { }; // TODO: Is there any way to bypass this? this is needed to set VISITOR_INFO1_LIVE which is required to read comments - std::string response; - DownloadResult result = download_to_string("https://youtube.com/subscription_manager?disable_polymer=1", response, std::move(additional_args), true); + std::string website_data; + DownloadResult result = download_to_string("https://youtube.com/subscription_manager?disable_polymer=1", website_data, std::move(additional_args), true); if(result != DownloadResult::OK) fprintf(stderr, "Failed to fetch cookies to view youtube comments\n"); } @@ -1143,6 +1148,247 @@ namespace QuickMedia { return PluginResult::OK; } + TrackResult YoutubeChannelPage::track(const std::string&) { + size_t channel_id_start = url.find("/channel/"); + if(channel_id_start == std::string::npos) { + show_notification("QuickMedia", "Unable to get channel id from " + url, Urgency::CRITICAL); + return TrackResult::ERR; + } + + channel_id_start += 9; + size_t channel_id_end = url.find('/', channel_id_start); + if(channel_id_end == std::string::npos) channel_id_end = url.size(); + std::string channel_id = url.substr(channel_id_start, channel_id_end - channel_id_start); + if(channel_id.empty()) { + show_notification("QuickMedia", "Unable to get channel id from " + url, Urgency::CRITICAL); + return TrackResult::ERR; + } + + Path subscriptions_path = get_storage_dir().join("subscriptions"); + if(create_directory_recursive(subscriptions_path) != 0) { + show_notification("QuickMedia", "Failed to create directory: " + subscriptions_path.data, Urgency::CRITICAL); + return TrackResult::ERR; + } + + subscriptions_path.join("youtube.txt"); + std::unordered_set<std::string> channel_ids; + std::string subscriptions_str; + + if(file_get_content(subscriptions_path, subscriptions_str) == 0) { + string_split(subscriptions_str, '\n', [&channel_ids](const char *str, size_t size) { + std::string line(str, size); + line = strip(line); + if(!line.empty()) + channel_ids.insert(std::move(line)); + return true; + }); + } + + auto it = channel_ids.find(channel_id); + if(it == channel_ids.end()) { + channel_ids.insert(channel_id); + show_notification("QuickMedia", "Subscribed", Urgency::LOW); + } else { + channel_ids.erase(it); + show_notification("QuickMedia", "Unsubscribed", Urgency::LOW); + } + + std::string channel_ids_str; + for(auto &it : channel_ids) { + if(!channel_ids_str.empty()) + channel_ids_str += '\n'; + channel_ids_str += std::move(it); + } + + if(file_overwrite_atomic(subscriptions_path, channel_ids_str) != 0) { + show_notification("QuickMedia", "Failed to update subscriptions list with " + channel_id, Urgency::CRITICAL); + return TrackResult::ERR; + } + + return TrackResult::OK; + } + + PluginResult YoutubeSubscriptionsPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) { + result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, url), nullptr}); + return PluginResult::OK; + } + + struct SubscriptionEntry { + std::string title; + std::string video_id; + time_t published = 0; + }; + + struct SubscriptionData { + SubscriptionEntry subscription_entry; + bool inside_entry = false; + }; + + static int string_view_equals(HtmlStringView *self, const char *sub) { + const size_t sub_len = strlen(sub); + return self->size == sub_len && memcmp(self->data, sub, sub_len) == 0; + } + + // Returns relative time as a string (approximation) + static std::string timestamp_to_relative_time_str(time_t seconds) { + time_t minutes = seconds / 60; + time_t hours = minutes / 60; + time_t days = hours / 24; + time_t months = days / 30; + time_t years = days / 365; + + if(years >= 1) + return std::to_string(years) + " year" + (years == 1 ? "" : "s") + " ago"; + else if(months >= 1) + return std::to_string(months) + " month" + (months == 1 ? "" : "s") + " ago"; + else if(days >= 1) + return std::to_string(days) + " day" + (days == 1 ? "" : "s") + " ago"; + else if(hours >= 1) + return std::to_string(hours) + " hour" + (hours == 1 ? "" : "s") + " ago"; + else if(minutes >= 1) + return std::to_string(minutes) + " minute" + (minutes == 1 ? "" : "s") + " ago"; + else + return std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s") + " ago"; + } + + PluginResult YoutubeSubscriptionsPage::lazy_fetch(BodyItems &result_items) { + Path subscriptions_path = get_storage_dir().join("subscriptions").join("youtube.txt"); + std::string subscriptions_str; + if(file_get_content(subscriptions_path, subscriptions_str) != 0) + return PluginResult::OK; + + // TODO: Make a async task pool to handle this more efficiently + std::vector<std::string> channel_ids; + string_split(subscriptions_str, '\n', [&channel_ids](const char *str, size_t size) { + std::string line(str, size); + line = strip(line); + if(!line.empty()) + channel_ids.push_back(std::move(line)); + return true; + }); + + std::vector<YoutubeSubscriptionTaskResult> task_results; + size_t async_task_index = 0; + const time_t time_now = time(nullptr); + + for(const std::string &channel_id : channel_ids) { + subscription_load_tasks[async_task_index] = AsyncTask<std::vector<YoutubeSubscriptionTaskResult>>([&channel_id, time_now]() -> std::vector<YoutubeSubscriptionTaskResult> { + std::string website_data; + DownloadResult result = download_to_string("https://www.youtube.com/feeds/videos.xml?channel_id=" + url_param_encode(channel_id), website_data, {}, false); + if(result != DownloadResult::OK) { + auto body_item = BodyItem::create("Failed to fetch videos for channel: " + channel_id); + return {YoutubeSubscriptionTaskResult{body_item, 0}}; + } + + std::vector<SubscriptionData> subscription_data_list; + + HtmlParser html_parser; + html_parser_init(&html_parser, website_data.data(), website_data.size(), [](HtmlParser *html_parser, HtmlParseType parse_type, void *userdata) { + std::vector<SubscriptionData> &subscription_data_list = *(std::vector<SubscriptionData>*)userdata; + + if(parse_type == HTML_PARSE_TAG_START && string_view_equals(&html_parser->tag_name, "entry")) { + subscription_data_list.push_back({}); + subscription_data_list.back().inside_entry = true; + return; + } else if(parse_type == HTML_PARSE_TAG_END && string_view_equals(&html_parser->tag_name, "entry")) { + subscription_data_list.back().inside_entry = false; + return; + } + + if(subscription_data_list.empty() || !subscription_data_list.back().inside_entry) + return; + + if(string_view_equals(&html_parser->tag_name, "title") && parse_type == HTML_PARSE_TAG_END) { + subscription_data_list.back().subscription_entry.title.assign(html_parser->text_stripped.data, html_parser->text_stripped.size); + } else if(string_view_equals(&html_parser->tag_name, "yt:videoId") && parse_type == HTML_PARSE_TAG_END) { + subscription_data_list.back().subscription_entry.video_id.assign(html_parser->text_stripped.data, html_parser->text_stripped.size); + } else if(string_view_equals(&html_parser->tag_name, "published") && parse_type == HTML_PARSE_TAG_END) { + std::string published_str(html_parser->text_stripped.data, html_parser->text_stripped.size); + + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + int second = 0; + sscanf(published_str.c_str(), "%d-%d-%dT%d:%d:%d", &year, &month, &day, &hour, &minute, &second); + if(year == 0) return; + + struct tm time; + memset(&time, 0, sizeof(time)); + time.tm_year = year - 1900; + time.tm_mon = month - 1; + time.tm_mday = day; + time.tm_hour = hour; + time.tm_min = minute; + time.tm_sec = second; + time_t unixtime = mktime(&time); + + struct tm entry_time; + localtime_r(&unixtime, &entry_time); + time_t unixtime_local = mktime(&entry_time); + + subscription_data_list.back().subscription_entry.published = unixtime_local; + } + }, &subscription_data_list); + html_parser_parse(&html_parser); + html_parser_deinit(&html_parser); + + /*std::sort(subscription_data_list.begin(), subscription_data_list.end(), [](const SubscriptionData &sub_data1, const SubscriptionData &sub_data2) { + return sub_data1.subscription_entry.published > sub_data2.subscription_entry.published; + });*/ + + std::vector<YoutubeSubscriptionTaskResult> results; + for(SubscriptionData &subscription_data : subscription_data_list) { + if(subscription_data.subscription_entry.title.empty() || subscription_data.subscription_entry.video_id.empty() || subscription_data.subscription_entry.published == 0) + continue; + + html_unescape_sequences(subscription_data.subscription_entry.title); + auto body_item = BodyItem::create(std::move(subscription_data.subscription_entry.title)); + body_item->set_description("Uploaded " + timestamp_to_relative_time_str(time_now - subscription_data.subscription_entry.published)); + body_item->set_description_color(sf::Color(179, 179, 179)); + body_item->url = "https://www.youtube.com/watch?v=" + subscription_data.subscription_entry.video_id; + body_item->thumbnail_url = "https://img.youtube.com/vi/" + subscription_data.subscription_entry.video_id + "/hqdefault.jpg"; + body_item->thumbnail_size = sf::Vector2i(175, 131); + results.push_back({std::move(body_item), subscription_data.subscription_entry.published}); + } + return results; + }); + ++async_task_index; + + if(async_task_index == subscription_load_tasks.size()) { + async_task_index = 0; + for(auto &load_task : subscription_load_tasks) { + if(!load_task.valid()) + continue; + + auto new_task_results = load_task.get(); + task_results.insert(task_results.end(), std::move_iterator(new_task_results.begin()), std::move_iterator(new_task_results.end())); + } + } + } + + for(size_t i = 0; i < async_task_index; ++i) { + auto &load_task = subscription_load_tasks[i]; + if(!load_task.valid()) + continue; + + auto new_task_results = load_task.get(); + task_results.insert(task_results.end(), std::move_iterator(new_task_results.begin()), std::move_iterator(new_task_results.end())); + } + + std::sort(task_results.begin(), task_results.end(), [](const YoutubeSubscriptionTaskResult &sub_data1, const YoutubeSubscriptionTaskResult &sub_data2) { + return sub_data1.timestamp > sub_data2.timestamp; + }); + + result_items.reserve(task_results.size()); + for(auto &task_result : task_results) { + result_items.push_back(std::move(task_result.body_item)); + } + + return PluginResult::OK; + } + PluginResult YoutubeRelatedVideosPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) { result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, url), nullptr}); return PluginResult::OK; @@ -1292,7 +1538,7 @@ namespace QuickMedia { return std::make_unique<YoutubeRelatedVideosPage>(program); } - std::unique_ptr<LazyFetchPage> YoutubeVideoPage::create_channels_page(Program *program, const std::string &channel_url) { + std::unique_ptr<Page> YoutubeVideoPage::create_channels_page(Program *program, const std::string &channel_url) { return std::make_unique<YoutubeChannelPage>(program, channel_url, "", "Channel videos"); } }
\ No newline at end of file |