From acb6ac0a04e800a79876908fd1fdb98dc7e93678 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 13 May 2021 23:30:20 +0200 Subject: Show local time for nyaa.si, soundcloud and spotify. Add sukebei --- TODO | 1 - include/Utils.hpp | 3 + plugins/NyaaSi.hpp | 13 +++-- src/QuickMedia.cpp | 11 ++-- src/Utils.cpp | 21 +++++++ src/plugins/NyaaSi.cpp | 138 +++++++++++++++++++++++++++++++-------------- src/plugins/Soundcloud.cpp | 20 +++++++ src/plugins/Spotify.cpp | 11 +++- src/plugins/Youtube.cpp | 22 +------- 9 files changed, 168 insertions(+), 72 deletions(-) diff --git a/TODO b/TODO index da8e59e..dcd998c 100644 --- a/TODO +++ b/TODO @@ -113,7 +113,6 @@ Add an option to select video resolution, if we want to use less power and less Check what happens with xsrf_token if comments are not fetched for a long time. Does it time out? if so do we need to refetch the video page to get the new token?. Add support for comments in live youtube videos, api is at: https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8. Make video visible when reading comments (youtube). -Convert nyaa.si/spotify/soundcloud date from ISO date string to local time. When ui is scaled then the predicted thumbnail size will be wrong since its scaled in Body but not in the plugins where they are requested. Check if get_page handlers in pages need to check if next batch is valid. If the server returns empty next batch we shouldn't fetch the first page... 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. diff --git a/include/Utils.hpp b/include/Utils.hpp index c1448f4..4a93109 100644 --- a/include/Utils.hpp +++ b/include/Utils.hpp @@ -1,8 +1,11 @@ #pragma once +#include + namespace QuickMedia { float get_ui_scale(); void show_virtual_keyboard(); void hide_virtual_keyboard(); bool is_touch_enabled(); + time_t iso_utc_to_unix_time(const char *time_str); } \ No newline at end of file diff --git a/plugins/NyaaSi.hpp b/plugins/NyaaSi.hpp index 13cc437..45c2f7b 100644 --- a/plugins/NyaaSi.hpp +++ b/plugins/NyaaSi.hpp @@ -3,18 +3,22 @@ #include "Page.hpp" namespace QuickMedia { + void get_nyaa_si_categories(BodyItems &result_items); + void get_sukebei_categories(BodyItems &result_items); + class NyaaSiCategoryPage : public Page { public: - NyaaSiCategoryPage(Program *program) : Page(program) {} - const char* get_title() const override { return "Select category"; } + NyaaSiCategoryPage(Program *program, bool is_sukebei) : Page(program), is_sukebei(is_sukebei) {} + const char* get_title() const override { return is_sukebei ? "Select sukebei category" : "Select nyaa.si category"; } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; bool clear_search_after_submit() override { return true; } - void get_categories(BodyItems &result_items); + + const bool is_sukebei; }; class NyaaSiSearchPage : public Page { public: - NyaaSiSearchPage(Program *program, std::string category_name, std::string category_id) : Page(program), category_name(std::move(category_name)), category_id(std::move(category_id)) {} + NyaaSiSearchPage(Program *program, std::string category_name, std::string category_id, std::string domain) : Page(program), category_name(std::move(category_name)), category_id(std::move(category_id)), domain(std::move(domain)) {} const char* get_title() const override { return category_name.c_str(); } bool search_is_filter() override { return false; } SearchResult search(const std::string &str, BodyItems &result_items) override; @@ -23,6 +27,7 @@ namespace QuickMedia { const std::string category_name; const std::string category_id; + const std::string domain; }; class NyaaSiTorrentPage : public Page { diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index c81e0c5..a58b3d8 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1076,10 +1076,13 @@ namespace QuickMedia { tabs.push_back(Tab{create_body(), std::make_unique(this, std::move(pages)), create_search_bar("Search...", 400)}); } else if(strcmp(plugin_name, "nyaa.si") == 0) { - auto category_page = std::make_unique(this); - auto categories_body = create_body(); - category_page->get_categories(categories_body->items); - tabs.push_back(Tab{std::move(categories_body), std::move(category_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + auto categories_nyaa_si_body = create_body(); + get_nyaa_si_categories(categories_nyaa_si_body->items); + tabs.push_back(Tab{std::move(categories_nyaa_si_body), std::make_unique(this, false), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + auto categories_sukebei_body = create_body(); + get_sukebei_categories(categories_sukebei_body->items); + tabs.push_back(Tab{std::move(categories_sukebei_body), std::make_unique(this, true), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "4chan") == 0) { auto boards_page = std::make_unique(this, resources_root); auto boards_body = create_body(); diff --git a/src/Utils.cpp b/src/Utils.cpp index 2a36d27..7ed6574 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -85,4 +85,25 @@ namespace QuickMedia { qm_enable_touch_set = true; return qm_enable_touch; } + + time_t iso_utc_to_unix_time(const char *time_str) { + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + int second = 0; + sscanf(time_str, "%d-%d-%dT%d:%d:%d", &year, &month, &day, &hour, &minute, &second); + if(year == 0) return 0; + + 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; + return timegm(&time); + } } \ No newline at end of file diff --git a/src/plugins/NyaaSi.cpp b/src/plugins/NyaaSi.cpp index 7a84346..2afedb3 100644 --- a/src/plugins/NyaaSi.cpp +++ b/src/plugins/NyaaSi.cpp @@ -39,16 +39,86 @@ namespace QuickMedia { return body_item; } + void get_nyaa_si_categories(BodyItems &result_items) { + result_items.push_back(create_front_page_item("All categories", "0_0")); + result_items.push_back(create_front_page_item("Anime", "1_0")); + result_items.push_back(create_front_page_item(" Anime - Music video", "1_1")); + result_items.push_back(create_front_page_item(" Anime - English translated", "1_2")); + result_items.push_back(create_front_page_item(" Anime - Non-english translated", "1_3")); + result_items.push_back(create_front_page_item(" Anime - Raw", "1_4")); + result_items.push_back(create_front_page_item("Audio", "2_0")); + result_items.push_back(create_front_page_item(" Audio - Lossless", "2_1")); + result_items.push_back(create_front_page_item(" Anime - Lossy", "2_2")); + result_items.push_back(create_front_page_item("Literature", "3_0")); + result_items.push_back(create_front_page_item(" Literature - English translated", "3_1")); + result_items.push_back(create_front_page_item(" Literature - Non-english translated", "3_1")); + result_items.push_back(create_front_page_item(" Literature - Raw", "3_3")); + result_items.push_back(create_front_page_item("Live Action", "4_0")); + result_items.push_back(create_front_page_item(" Live Action - English translated", "4_1")); + result_items.push_back(create_front_page_item(" Live Action - Non-english translated", "4_3")); + result_items.push_back(create_front_page_item(" Live Action - Idol/Promotional video", "4_2")); + result_items.push_back(create_front_page_item(" Live Action - Raw", "4_4")); + result_items.push_back(create_front_page_item("Pictures", "5_0")); + result_items.push_back(create_front_page_item(" Pictures - Graphics", "5_1")); + result_items.push_back(create_front_page_item(" Pictures - Photos", "5_2")); + result_items.push_back(create_front_page_item("Software", "6_0")); + result_items.push_back(create_front_page_item(" Software - Applications", "6_1")); + result_items.push_back(create_front_page_item(" Software - Games", "6_2")); + } + + void get_sukebei_categories(BodyItems &result_items) { + result_items.push_back(create_front_page_item("All categories", "0_0")); + result_items.push_back(create_front_page_item("Art", "1_0")); + result_items.push_back(create_front_page_item(" Anime", "1_1")); + result_items.push_back(create_front_page_item(" Doujinshi", "1_2")); + result_items.push_back(create_front_page_item(" Games", "1_3")); + result_items.push_back(create_front_page_item(" Manga", "1_4")); + result_items.push_back(create_front_page_item(" Pictures", "1_5")); + result_items.push_back(create_front_page_item("Real Life", "2_0")); + result_items.push_back(create_front_page_item(" Photobooks and Pictures", "2_1")); + result_items.push_back(create_front_page_item(" Videos", "2_2")); + } + + static time_t nyaa_si_time_to_unix_time(const char *time_str) { + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + sscanf(time_str, "%d-%d-%d %d:%d", &year, &month, &day, &hour, &minute); + if(year == 0) return 0; + + 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 = 0; + return timegm(&time); + } + + static std::string unix_time_to_local_time_str(time_t unix_time) { + struct tm time_tm; + localtime_r(&unix_time, &time_tm); + char time_str[128] = {0}; + strftime(time_str, sizeof(time_str) - 1, "%Y-%m-%d %H:%M", &time_tm); + return time_str; + } + // TODO: Also show the number of comments for each torrent. TODO: Optimize? // TODO: Show each field as seperate columns instead of seperating by | - static SearchResult search_page(const std::string &list_url, const std::string &text, int page, BodyItems &result_items) { - std::string full_url = "https://nyaa.si/?c=" + list_url + "&f=0&p=" + std::to_string(page) + "&q="; + static SearchResult search_page(const std::string &domain, const std::string &list_url, const std::string &text, int page, BodyItems &result_items) { + std::string full_url = "https://" + domain + "/?c=" + list_url + "&f=0&p=" + std::to_string(page) + "&q="; full_url += url_param_encode(text); std::string website_data; if(download_to_string(full_url, website_data, {}, true) != DownloadResult::OK) return SearchResult::NET_ERR; + const bool is_sukebei = (domain == "sukebei.nyaa.si"); + size_t tbody_begin = website_data.find(""); if(tbody_begin == std::string::npos) return SearchResult::OK; @@ -161,11 +231,11 @@ namespace QuickMedia { index = tr_end + 5; - std::string description = "Size: " + size + " | Published: " + timestamp + " | Seeders: " + seeders + " | Leechers: " + leechers + " | Completed: " + completed; + std::string description = "Size: " + size + " | Published: " + unix_time_to_local_time_str(nyaa_si_time_to_unix_time(timestamp.c_str())) + " | Seeders: " + seeders + " | Leechers: " + leechers + " | Completed: " + completed; auto body_item = BodyItem::create(std::move(title)); - body_item->thumbnail_url = "https://nyaa.si/static/img/icons/nyaa/" + website_data.substr(category_begin + 4, category_end - (category_begin + 4)) + ".png"; + body_item->thumbnail_url = "https://" + domain + "/static/img/icons/" + (is_sukebei ? "sukebei" : "nyaa") + "/" + website_data.substr(category_begin + 4, category_end - (category_begin + 4)) + ".png"; body_item->set_description(std::move(description)); - body_item->url = "https://nyaa.si" + std::move(view_url); + body_item->url = "https://" + domain + std::move(view_url); if(is_trusted) body_item->set_title_color(sf::Color(43, 255, 47)); else if(is_remake) @@ -178,51 +248,31 @@ namespace QuickMedia { } PluginResult NyaaSiCategoryPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + std::string domain = is_sukebei ? "sukebei.nyaa.si" : "nyaa.si"; + BodyItems result_items; - SearchResult search_result = search_page(url, "", 1, result_items); + SearchResult search_result = search_page(domain, url, "", 1, result_items); if(search_result != SearchResult::OK) return search_result_to_plugin_result(search_result); auto body = create_body(); body->items = std::move(result_items); - result_tabs.push_back(Tab{std::move(body), std::make_unique(program, strip(title), url), create_search_bar("Search...", 300)}); + result_tabs.push_back(Tab{std::move(body), std::make_unique(program, strip(title), url, std::move(domain)), create_search_bar("Search...", 300)}); return PluginResult::OK; } - void NyaaSiCategoryPage::get_categories(BodyItems &result_items) { - result_items.push_back(create_front_page_item("All categories", "0_0")); - result_items.push_back(create_front_page_item("Anime", "1_0")); - result_items.push_back(create_front_page_item(" Anime - Music video", "1_1")); - result_items.push_back(create_front_page_item(" Anime - English translated", "1_2")); - result_items.push_back(create_front_page_item(" Anime - Non-english translated", "1_3")); - result_items.push_back(create_front_page_item(" Anime - Raw", "1_4")); - result_items.push_back(create_front_page_item("Audio", "2_0")); - result_items.push_back(create_front_page_item(" Audio - Lossless", "2_1")); - result_items.push_back(create_front_page_item(" Anime - Lossy", "2_2")); - result_items.push_back(create_front_page_item("Literature", "3_0")); - result_items.push_back(create_front_page_item(" Literature - English translated", "3_1")); - result_items.push_back(create_front_page_item(" Literature - Non-english translated", "3_1")); - result_items.push_back(create_front_page_item(" Literature - Raw", "3_3")); - result_items.push_back(create_front_page_item("Live Action", "4_0")); - result_items.push_back(create_front_page_item(" Live Action - English translated", "4_1")); - result_items.push_back(create_front_page_item(" Live Action - Non-english translated", "4_3")); - result_items.push_back(create_front_page_item(" Live Action - Idol/Promotional video", "4_2")); - result_items.push_back(create_front_page_item(" Live Action - Raw", "4_4")); - result_items.push_back(create_front_page_item("Pictures", "5_0")); - result_items.push_back(create_front_page_item(" Pictures - Graphics", "5_1")); - result_items.push_back(create_front_page_item(" Pictures - Photos", "5_2")); - result_items.push_back(create_front_page_item("Software", "6_0")); - result_items.push_back(create_front_page_item(" Software - Applications", "6_1")); - result_items.push_back(create_front_page_item(" Software - Games", "6_2")); - } - SearchResult NyaaSiSearchPage::search(const std::string &str, BodyItems &result_items) { - return search_page(category_id, str, 1, result_items); + return search_page(domain, category_id, str, 1, result_items); } PluginResult NyaaSiSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { - return search_result_to_plugin_result(search_page(category_id, str, 1 + page, result_items)); + return search_result_to_plugin_result(search_page(domain, category_id, str, 1 + page, result_items)); } + struct ResultItemExtra { + BodyItems *result_items; + const std::string *domain; + }; + PluginResult NyaaSiSearchPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { size_t comments_start_index; std::string title; @@ -232,6 +282,10 @@ namespace QuickMedia { std::string magnet_url; std::string description; + ResultItemExtra result_item_extra; + result_item_extra.result_items = &result_items; + result_item_extra.domain = &domain; + std::string website_data; if(download_to_string(url, website_data, {}, true) != DownloadResult::OK) return PluginResult::NET_ERR; @@ -261,16 +315,16 @@ namespace QuickMedia { result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='panel-body']//div[class='row']//a", [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItems*)userdata; + ResultItemExtra *item_data = (ResultItemExtra*)userdata; const char *href = quickmedia_html_node_get_attribute_value(node, "href"); const char *text = quickmedia_html_node_get_text(node); - if(item_data->empty() && href && text && strncmp(href, "/user/", 6) == 0) { + if(item_data->result_items->empty() && href && text && strncmp(href, "/user/", 6) == 0) { auto body_item = BodyItem::create(""); body_item->set_description("Submitter: " + strip(text)); - body_item->url = "https://nyaa.si/" + std::string(href); - item_data->push_back(std::move(body_item)); + body_item->url = "https://" + *item_data->domain + "/" + std::string(href); + item_data->result_items->push_back(std::move(body_item)); } - }, &result_items); + }, &result_item_extra); if(result != 0) goto cleanup; @@ -313,7 +367,7 @@ namespace QuickMedia { goto cleanup; if(magnet_url.empty()) { - fprintf(stderr, "Error: nyaa.si: failed to get magnet link\n"); + fprintf(stderr, "Error: %s: failed to get magnet link\n", domain.c_str()); result = -1; goto cleanup; } diff --git a/src/plugins/Soundcloud.cpp b/src/plugins/Soundcloud.cpp index 9a5fe64..9c2d5f4 100644 --- a/src/plugins/Soundcloud.cpp +++ b/src/plugins/Soundcloud.cpp @@ -1,6 +1,7 @@ #include "../../plugins/Soundcloud.hpp" #include "../../include/NetUtils.hpp" #include "../../include/StringUtils.hpp" +#include "../../include/Utils.hpp" #include "../../include/Scale.hpp" #include @@ -64,6 +65,14 @@ namespace QuickMedia { return ""; } + static std::string unix_time_to_local_time_str(time_t unix_time) { + struct tm time_tm; + localtime_r(&unix_time, &time_tm); + char time_str[128] = {0}; + strftime(time_str, sizeof(time_str) - 1, "%Y-%m-%d %H:%M", &time_tm); + return time_str; + } + static std::shared_ptr parse_collection_item(const Json::Value &item_json) { std::string title; @@ -105,6 +114,12 @@ namespace QuickMedia { auto body_item = BodyItem::create(std::move(title)); std::string description; + const Json::Value &last_modified_json = item_json["last_modified"]; + if(last_modified_json.isString()) { + const time_t unix_time = iso_utc_to_unix_time(last_modified_json.asCString()); + description = "Updated " + unix_time_to_local_time_str(unix_time); + } + const Json::Value &media_json = item_json["media"]; if(media_json.isObject()) body_item->url = get_best_transcoding_audio_url(media_json); @@ -129,6 +144,8 @@ namespace QuickMedia { } num_tracks = tracks_json.size(); + if(!description.empty()) + description += '\n'; description = "Playlist with " + std::to_string(num_tracks) + " track" + (num_tracks == 1 ? "" : "s"); body_item->extra = std::move(playlist); body_item->url = "track"; @@ -167,6 +184,9 @@ namespace QuickMedia { body_item->thumbnail_size.x = 100; body_item->thumbnail_size.y = 100; body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; + } else { + body_item->thumbnail_size.x = 100; + body_item->thumbnail_size.y = 100; } if(username_json.isString()) { diff --git a/src/plugins/Spotify.cpp b/src/plugins/Spotify.cpp index f56ed6c..d41446b 100644 --- a/src/plugins/Spotify.cpp +++ b/src/plugins/Spotify.cpp @@ -1,5 +1,6 @@ #include "../../plugins/Spotify.hpp" #include "../../include/NetUtils.hpp" +#include "../../include/Utils.hpp" #include "../../include/Scale.hpp" namespace QuickMedia { @@ -171,6 +172,14 @@ namespace QuickMedia { return result; } + static std::string unix_time_to_local_time_str(time_t unix_time) { + struct tm time_tm; + localtime_r(&unix_time, &time_tm); + char time_str[128] = {0}; + strftime(time_str, sizeof(time_str) - 1, "%Y-%m-%d %H:%M", &time_tm); + return time_str; + } + PluginResult SpotifyEpisodeListPage::get_page(const std::string &, int page, BodyItems &result_items) { std::string request_url = "https://api-partner.spotify.com/pathfinder/v1/query?operationName=queryShowEpisodes&variables="; request_url += url_param_encode("{\"uri\":\"" + url + "\",\"offset\":" + std::to_string(page * 50) + ",\"limit\":50}"); @@ -242,7 +251,7 @@ namespace QuickMedia { if(release_data_json.isObject()) { const Json::Value &iso_string_json = release_data_json["isoString"]; if(iso_string_json.isString()) - time += iso_string_json.asString(); + time += unix_time_to_local_time_str(iso_utc_to_unix_time(iso_string_json.asCString())); } const Json::Value &duration_json = episode_json["duration"]; diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 0fcf61d..a93dcb6 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -4,6 +4,7 @@ #include "../../include/StringUtils.hpp" #include "../../include/Scale.hpp" #include "../../include/Notification.hpp" +#include "../../include/Utils.hpp" extern "C" { #include } @@ -1375,26 +1376,7 @@ namespace QuickMedia { subscription_data.subscription_entry.back().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; - - subscription_data.subscription_entry.back().published = timegm(&time); + subscription_data.subscription_entry.back().published = iso_utc_to_unix_time(published_str.c_str()); } }, &subscription_data); -- cgit v1.2.3