aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Body.cpp5
-rw-r--r--src/QuickMedia.cpp14
-rw-r--r--src/StringUtils.cpp11
-rw-r--r--src/plugins/Manga.cpp8
-rw-r--r--src/plugins/Matrix.cpp5
-rw-r--r--src/plugins/Soundcloud.cpp10
-rw-r--r--src/plugins/Youtube.cpp256
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