diff options
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | TODO | 3 | ||||
m--------- | depends/html-parser | 0 | ||||
-rw-r--r-- | include/StringUtils.hpp | 2 | ||||
-rw-r--r-- | plugins/ImageBoard.hpp | 2 | ||||
-rw-r--r-- | plugins/Manga.hpp | 6 | ||||
-rw-r--r-- | plugins/Matrix.hpp | 2 | ||||
-rw-r--r-- | plugins/Page.hpp | 9 | ||||
-rw-r--r-- | plugins/Pornhub.hpp | 2 | ||||
-rw-r--r-- | plugins/Soundcloud.hpp | 4 | ||||
-rw-r--r-- | plugins/Spankbang.hpp | 2 | ||||
-rw-r--r-- | plugins/Spotify.hpp | 2 | ||||
-rw-r--r-- | plugins/Youtube.hpp | 25 | ||||
-rw-r--r-- | shaders/circle_mask.glsl | 2 | ||||
-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 |
21 files changed, 329 insertions, 44 deletions
@@ -35,7 +35,8 @@ Press `Ctrl + D` to clear the search input text.\ Press `Ctrl + F` to switch between window mode and fullscreen mode when watching a video.\ Press `Space` to pause/unpause a video.\ Press `Ctrl + R` to show video comments, related videos or video channel when watching a video (if supported).\ -Press `Ctrl + T` when hovering over a manga chapter to start tracking manga after that chapter. This only works if AutoMedia is installed. +Press `Ctrl + T` when hovering over a manga chapter to start tracking manga after that chapter. This only works if AutoMedia is installed.\ +Press `Ctrl + T` when viewing a youtube channels page to subscribe to that channel or to unsubscribe.\ Press `Backspace` to return to the preview item when reading replies in image board threads.\ Press `R` to paste the post number of the selected post into the post field (image boards).\ Press `I` to begin writing a post to a thread (image boards), press `ESC` to cancel.\ @@ -135,4 +135,5 @@ Check if get_page handlers in pages need to check if next batch is valid. If the Cloudflare kicks in when downloading manga on manganelo.. figure out a way to bypass it. This doesn't seem to happen when using python requests as is done in AutoMedia. Replace cppcodec with another library for base64 url encoding/decoding. Its way too large for what it does. Revert back to old fuzzy search code or use levenshtein distance, then reorder items by best match. This could be done by having a second vector of indices and use that vector everywhere body items by index is accessed (including selected_item). Also perform the search in Body::draw when search term has been modified. This allows us to automatically update that new vector. -Using a VertexBuffer in Text makes the text quickly glitch out. Why does this happen?
\ No newline at end of file +Using a VertexBuffer in Text makes the text quickly glitch out. Why does this happen? +Update subscriptions page either with f5 and automatically update it when adding/removing subscriptions.
\ No newline at end of file diff --git a/depends/html-parser b/depends/html-parser -Subproject 60bde2dc9bb3a6e0276d624fe3490b891c3a7fc +Subproject 4a2f50f00529aa0894486a099b721826add9205 diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp index e97a423..6df16db 100644 --- a/include/StringUtils.hpp +++ b/include/StringUtils.hpp @@ -9,6 +9,8 @@ namespace QuickMedia { void string_split(const std::string &str, char delimiter, StringSplitCallback callback_func); // Returns the number of replaced substrings + size_t string_replace_all(std::string &str, char old_char, char new_char); + // Returns the number of replaced substrings size_t string_replace_all(std::string &str, char old_char, const std::string &new_str); // Returns the number of replaced substrings size_t string_replace_all(std::string &str, const std::string &old_str, const std::string &new_str); diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp index bd47bec..9d4c123 100644 --- a/plugins/ImageBoard.hpp +++ b/plugins/ImageBoard.hpp @@ -18,7 +18,7 @@ namespace QuickMedia { BodyItems get_related_media(const std::string &url, std::string &channel_url) override; std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) override; - std::unique_ptr<LazyFetchPage> create_channels_page(Program*, const std::string&) override { + std::unique_ptr<Page> create_channels_page(Program*, const std::string&) override { return nullptr; } std::string get_url() override { return video_url; } diff --git a/plugins/Manga.hpp b/plugins/Manga.hpp index 388ce66..ee7638b 100644 --- a/plugins/Manga.hpp +++ b/plugins/Manga.hpp @@ -47,11 +47,13 @@ namespace QuickMedia { int chapter_num_pages; }; - class MangaChaptersPage : public TrackablePage { + class MangaChaptersPage : public Page, public TrackablePage { public: - MangaChaptersPage(Program *program, std::string manga_name, std::string manga_url) : TrackablePage(program, std::move(manga_name), std::move(manga_url)) {} + MangaChaptersPage(Program *program, std::string manga_name, std::string manga_url) : Page(program), TrackablePage(std::move(manga_name), std::move(manga_url)) {} + const char* get_title() const override { return content_title.c_str(); } TrackResult track(const std::string &str) override; void on_navigate_to_page(Body *body) override; + bool is_trackable() const override { return true; } protected: virtual bool extract_id_from_url(const std::string &url, std::string &manga_id) const = 0; virtual const char* get_service_name() const = 0; diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 1063e0e..fb52744 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -420,7 +420,7 @@ namespace QuickMedia { std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program*, const std::string&, const std::string&) override { return nullptr; } - std::unique_ptr<LazyFetchPage> create_channels_page(Program*, const std::string&) override { + std::unique_ptr<Page> create_channels_page(Program*, const std::string&) override { return nullptr; } std::string get_url() override { return url; } diff --git a/plugins/Page.hpp b/plugins/Page.hpp index 6599fe4..175b44b 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -72,11 +72,10 @@ namespace QuickMedia { ERR }; - class TrackablePage : public Page { + class TrackablePage { public: - TrackablePage(Program *program, std::string content_title, std::string content_url) : Page(program), content_title(std::move(content_title)), content_url(std::move(content_url)) {} - const char* get_title() const override { return content_title.c_str(); } - bool is_trackable() const override { return true; } + TrackablePage(std::string content_title, std::string content_url) : content_title(std::move(content_title)), content_url(std::move(content_url)) {} + virtual ~TrackablePage() = default; virtual TrackResult track(const std::string &str) = 0; const std::string content_title; @@ -110,7 +109,7 @@ namespace QuickMedia { // Return nullptr if the service doesn't support related videos page virtual std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) = 0; // Return nullptr if the service doesn't support channels page - virtual std::unique_ptr<LazyFetchPage> create_channels_page(Program *program, const std::string &channel_url) = 0; + virtual std::unique_ptr<Page> create_channels_page(Program *program, const std::string &channel_url) = 0; virtual std::string get_url() = 0; virtual std::string url_get_playable_url(const std::string &url) { return url; } virtual bool video_should_be_skipped(const std::string &url) { (void)url; return false; } diff --git a/plugins/Pornhub.hpp b/plugins/Pornhub.hpp index 87e33da..8f6f563 100644 --- a/plugins/Pornhub.hpp +++ b/plugins/Pornhub.hpp @@ -26,7 +26,7 @@ namespace QuickMedia { BodyItems get_related_media(const std::string &url, std::string &channel_url) override; std::unique_ptr<Page> create_search_page(Program *program, int &search_delay) override; std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) override; - std::unique_ptr<LazyFetchPage> create_channels_page(Program*, const std::string&) override { + std::unique_ptr<Page> create_channels_page(Program*, const std::string&) override { return nullptr; } std::string get_url() override { return url; } diff --git a/plugins/Soundcloud.hpp b/plugins/Soundcloud.hpp index 24dc051..0f397a1 100644 --- a/plugins/Soundcloud.hpp +++ b/plugins/Soundcloud.hpp @@ -25,7 +25,7 @@ namespace QuickMedia { private: SoundcloudPage submit_page; std::string query_urn; - std::vector<AsyncTask<std::string>> async_download_threads; + std::vector<AsyncTask<std::string>> async_download_tasks; }; class SoundcloudUserPage : public SoundcloudPage { @@ -56,7 +56,7 @@ namespace QuickMedia { const char* get_title() const override { return ""; } bool autoplay_next_item() override { return true; } std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *, const std::string &, const std::string &) override { return nullptr; } - std::unique_ptr<LazyFetchPage> create_channels_page(Program *, const std::string &) override { return nullptr; } + std::unique_ptr<Page> create_channels_page(Program *, const std::string &) override { return nullptr; } std::string get_url() override { return url; } std::string url_get_playable_url(const std::string &url) override; bool video_should_be_skipped(const std::string &url) override { return url == "track" || url.find("/stream/users/") != std::string::npos; } diff --git a/plugins/Spankbang.hpp b/plugins/Spankbang.hpp index 95b8820..06ea509 100644 --- a/plugins/Spankbang.hpp +++ b/plugins/Spankbang.hpp @@ -26,7 +26,7 @@ namespace QuickMedia { BodyItems get_related_media(const std::string &url, std::string &channel_url) override; std::unique_ptr<Page> create_search_page(Program *program, int &search_delay) override; std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) override; - std::unique_ptr<LazyFetchPage> create_channels_page(Program*, const std::string&) override { + std::unique_ptr<Page> create_channels_page(Program*, const std::string&) override { return nullptr; } std::string get_url() override { return url; } diff --git a/plugins/Spotify.hpp b/plugins/Spotify.hpp index 89f8f3d..66cc992 100644 --- a/plugins/Spotify.hpp +++ b/plugins/Spotify.hpp @@ -41,7 +41,7 @@ namespace QuickMedia { SpotifyAudioPage(Program *program, const std::string &url) : VideoPage(program), url(url) {} const char* get_title() const override { return ""; } std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *, const std::string &, const std::string &) override { return nullptr; } - std::unique_ptr<LazyFetchPage> create_channels_page(Program *, const std::string &) override { return nullptr; } + std::unique_ptr<Page> create_channels_page(Program *, const std::string &) override { return nullptr; } std::string get_url() override { return url; } private: std::string url; diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index 4691f04..4be5339 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -1,6 +1,7 @@ #pragma once #include "Page.hpp" +#include "../include/AsyncTask.hpp" #include <unordered_set> namespace QuickMedia { @@ -47,9 +48,9 @@ namespace QuickMedia { std::string continuation_token; }; - class YoutubeChannelPage : public LazyFetchPage { + class YoutubeChannelPage : public LazyFetchPage, public TrackablePage { public: - YoutubeChannelPage(Program *program, std::string url, std::string continuation_token, std::string title) : LazyFetchPage(program), url(std::move(url)), continuation_token(std::move(continuation_token)), title(std::move(title)) {} + YoutubeChannelPage(Program *program, std::string url, std::string continuation_token, std::string title) : LazyFetchPage(program), TrackablePage(title, url), url(url), continuation_token(std::move(continuation_token)), title(title) {} const char* get_title() const override { return title.c_str(); } bool search_is_filter() override { return false; } SearchResult search(const std::string &str, BodyItems &result_items) override; @@ -57,6 +58,9 @@ namespace QuickMedia { PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; PluginResult lazy_fetch(BodyItems &result_items) override; + TrackResult track(const std::string &str) override; + bool is_trackable() const override { return true; } + std::unordered_set<std::string> added_videos; private: PluginResult search_get_continuation(const std::string &url, const std::string &continuation_token, BodyItems &result_items); @@ -67,6 +71,21 @@ namespace QuickMedia { int current_page = 0; }; + struct YoutubeSubscriptionTaskResult { + std::shared_ptr<BodyItem> body_item; + time_t timestamp = 0; + }; + + class YoutubeSubscriptionsPage : public LazyFetchPage { + public: + YoutubeSubscriptionsPage(Program *program) : LazyFetchPage(program) {} + const char* get_title() const override { return "Subscriptions"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + PluginResult lazy_fetch(BodyItems &result_items) override; + private: + std::array<AsyncTask<std::vector<YoutubeSubscriptionTaskResult>>, 4> subscription_load_tasks; // TODO: Use multiple curl outputs instead? + }; + class YoutubeRelatedVideosPage : public RelatedVideosPage { public: YoutubeRelatedVideosPage(Program *program) : RelatedVideosPage(program) {} @@ -81,7 +100,7 @@ namespace QuickMedia { std::unique_ptr<Page> create_search_page(Program *program, int &search_delay) override; std::unique_ptr<Page> create_comments_page(Program *program) override; std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) override; - std::unique_ptr<LazyFetchPage> create_channels_page(Program *program, const std::string &channel_url) override; + std::unique_ptr<Page> create_channels_page(Program *program, const std::string &channel_url) override; std::string get_url() override { return url; } private: std::string xsrf_token; diff --git a/shaders/circle_mask.glsl b/shaders/circle_mask.glsl index 0266d2c..198191e 100644 --- a/shaders/circle_mask.glsl +++ b/shaders/circle_mask.glsl @@ -2,7 +2,7 @@ uniform sampler2D texture; uniform vec2 resolution; vec4 circle(vec2 uv, vec2 pos, float rad, vec4 color) { - float d = length(pos - uv) - rad; + float d = smoothstep(0.0, 2.0, length(pos - uv) - rad); float t = clamp(d, 0.0, 1.0); return vec4(color.rgb, color.a * (1.0 - t)); } 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 |