aboutsummaryrefslogtreecommitdiff
path: root/src/plugins/Youtube.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/Youtube.cpp')
-rw-r--r--src/plugins/Youtube.cpp256
1 files changed, 251 insertions, 5 deletions
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