diff options
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | TODO | 2 | ||||
-rw-r--r-- | include/Path.hpp | 2 | ||||
-rw-r--r-- | include/SearchBar.hpp | 1 | ||||
-rw-r--r-- | include/StringUtils.hpp | 1 | ||||
-rw-r--r-- | plugins/MangaGeneric.hpp | 25 | ||||
-rw-r--r-- | plugins/Matrix.hpp | 3 | ||||
-rw-r--r-- | plugins/Page.hpp | 2 | ||||
-rw-r--r-- | plugins/Soundcloud.hpp | 9 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 44 | ||||
-rw-r--r-- | src/SearchBar.cpp | 8 | ||||
-rw-r--r-- | src/StringUtils.cpp | 5 | ||||
-rw-r--r-- | src/plugins/MangaGeneric.cpp | 159 | ||||
-rw-r--r-- | src/plugins/Matrix.cpp | 33 | ||||
-rw-r--r-- | src/plugins/Soundcloud.cpp | 68 |
15 files changed, 278 insertions, 86 deletions
@@ -62,7 +62,7 @@ In matrix you can select a message with enter to open the url in the message (or `/logout` to logout.\ `/leave` to leave the current room.\ `/me [text]` to send a message of type "m.emote".\ -`/react [text]` to react to the selected message. +`/react [text]` to react to the selected message or if you are replying to message then it reacts to that message. ## Mangadex To search for manga with mangadex, you need to be logged into mangadex in your browser and copy the `mangadex_rememberme_token` cookie from developer tools and store it in `$HOME/.config/quickmedia/credentials/mangadex.json` under the key `rememberme_token`. Here is an example what the file should look like: @@ -90,7 +90,7 @@ Make /logout work everywhere, not only in room message input (or add a logout bu Add a notifications tab to show messages that mention us in all rooms (and then press enter to go to that message in that room), also add a unread/mentioned rooms list tab to only show rooms with unread messages or mentions. Disable message input in matrix when muted. Preview rooms? -Handle matrix token being invalidated while running and not running. +Handle matrix token being invalidated while running. Update upload limit if its updated on the server. Editing a reply removes reply formatting (both in body and formatted_body). Element also does this when you edit a reply twice. This breaks element mobile that is unable to display replied-to messages without correct formatting (doesn't fetch the replied-to message). This also removes the mentioned name which breaks mention for reply. diff --git a/include/Path.hpp b/include/Path.hpp index 19c8eb9..571fb9e 100644 --- a/include/Path.hpp +++ b/include/Path.hpp @@ -39,7 +39,7 @@ namespace QuickMedia { Path parent() { size_t slash_index = data.rfind('/'); - if(slash_index != std::string::npos || slash_index == 0) + if(slash_index != std::string::npos && slash_index > 0) return Path(data.substr(0, slash_index)); return Path("/"); } diff --git a/include/SearchBar.hpp b/include/SearchBar.hpp index 76ad24b..06c4c98 100644 --- a/include/SearchBar.hpp +++ b/include/SearchBar.hpp @@ -70,5 +70,6 @@ namespace QuickMedia { bool mouse_left_inside; float vertical_pos; sf::Clock time_since_search_update; + sf::Vector2u prev_window_size; }; }
\ No newline at end of file diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp index 97c73dd..e97a423 100644 --- a/include/StringUtils.hpp +++ b/include/StringUtils.hpp @@ -13,5 +13,6 @@ namespace QuickMedia { // Returns the number of replaced substrings size_t string_replace_all(std::string &str, const std::string &old_str, const std::string &new_str); std::string strip(const std::string &str); + bool string_starts_with(const std::string &str, const char *sub); bool string_ends_with(const std::string &str, const std::string &ends_with_str); }
\ No newline at end of file diff --git a/plugins/MangaGeneric.hpp b/plugins/MangaGeneric.hpp index 98d06d8..a03756d 100644 --- a/plugins/MangaGeneric.hpp +++ b/plugins/MangaGeneric.hpp @@ -23,13 +23,19 @@ namespace QuickMedia { const char *field_name = nullptr; }; - // If |field_contains| is null, then any matching query is added. If |field_name| is "text", then the inner text is used. struct ThumbnailQuery { const char *html_query = nullptr; const char *field_name = nullptr; const char *field_contains = nullptr; }; + struct AuthorsQuery { + const char *html_query = nullptr; + const char *title_field = nullptr; + const char *url_field = nullptr; + const char *url_contains = nullptr; + }; + struct ListChaptersQuery { const char *html_query = nullptr; const char *title_field = nullptr; @@ -92,6 +98,7 @@ namespace QuickMedia { const char* get_title() const override { return "All"; } bool search_is_filter() override { return false; } SearchResult search(const std::string &str, BodyItems &result_items) override; + PluginResult get_page(const std::string &url, BodyItems &result_items); PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); }; @@ -106,6 +113,9 @@ namespace QuickMedia { MangaGenericSearchPage& description_handler(std::vector<DescriptionQuery> queries); // This is optional. MangaGenericSearchPage& thumbnail_handler(std::vector<ThumbnailQuery> queries); + // If |url_contains| is null, then any matching query is added. If |title_field| is "text", then the inner text is used. + // This is optional. + MangaGenericSearchPage& authors_handler(const char *html_query, const char *title_field, const char *url_field, const char *url_contains); // If |url_contains| is null, then any matching query is added. If |title_field| is "text", then the inner text is used. // This is required. @@ -141,6 +151,7 @@ namespace QuickMedia { std::vector<TextQuery> text_queries; std::vector<DescriptionQuery> description_queries; std::vector<ThumbnailQuery> thumbnail_queries; + AuthorsQuery authors_query; ListChaptersQuery list_chapters_query; ListPageQuery list_page_query; MangaIdExtractor manga_id_extractor; @@ -162,6 +173,18 @@ namespace QuickMedia { bool fail_on_http_error; }; + class MangaGenericCreatorPage : public LazyFetchPage { + public: + MangaGenericCreatorPage(Program *program, MangaGenericSearchPage *search_page, Creator creator) : LazyFetchPage(program), search_page(search_page), creator(std::move(creator)) {} + const char* get_title() const override { return creator.name.c_str(); } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + PluginResult lazy_fetch(BodyItems &result_items) override; + sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); }; + private: + MangaGenericSearchPage *search_page; + Creator creator; + }; + class MangaGenericImagesPage : public MangaImagesPage { public: MangaGenericImagesPage(Program *program, std::string manga_name, std::string chapter_name, std::string url, const char *service_name, const std::string &website_url, const ListPageQuery *list_page_query, bool fail_on_http_error) : diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 0659983..f76d70d 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -477,7 +477,8 @@ namespace QuickMedia { class Matrix { public: - void start_sync(MatrixDelegate *delegate, bool &cached); + // TODO: Make this return the Matrix object instead, to force users to call start_sync + bool start_sync(MatrixDelegate *delegate, bool &cached); void stop_sync(); bool is_initial_sync_finished() const; // Returns true if initial sync failed, and |err_msg| is set to the error reason in that case diff --git a/plugins/Page.hpp b/plugins/Page.hpp index 5c0a558..6599fe4 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -89,6 +89,8 @@ namespace QuickMedia { virtual bool search_is_filter() override { return true; } bool is_lazy_fetch_page() const override { return true; } virtual PluginResult lazy_fetch(BodyItems &result_items) = 0; + // If this returns true then |lazy_fetch| is not meant to return results but async background load the page. This can be used to fetch API keys for example + virtual bool lazy_fetch_is_loader() { return false; } }; class RelatedVideosPage : public Page { diff --git a/plugins/Soundcloud.hpp b/plugins/Soundcloud.hpp index e04d409..bb23efb 100644 --- a/plugins/Soundcloud.hpp +++ b/plugins/Soundcloud.hpp @@ -7,17 +7,22 @@ namespace QuickMedia { public: SoundcloudPage(Program *program) : Page(program) {} virtual ~SoundcloudPage() = default; + virtual const char* get_title() const override { return ""; } PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; }; - class SoundcloudSearchPage : public SoundcloudPage { + class SoundcloudSearchPage : public LazyFetchPage { public: - SoundcloudSearchPage(Program *program) : SoundcloudPage(program) {} + SoundcloudSearchPage(Program *program) : LazyFetchPage(program), submit_page(program) {} const char* get_title() const override { return "Search"; } bool search_is_filter() override { return false; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; SearchResult search(const std::string &str, BodyItems &result_items) override; PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; + PluginResult lazy_fetch(BodyItems &result_items) override; + bool lazy_fetch_is_loader() override { return true; } private: + SoundcloudPage submit_page; std::string query_urn; }; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index eb82a54..e2ee0ec 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -745,6 +745,7 @@ namespace QuickMedia { {"//div[id='book_list']//div[class='media']//img", "src", nullptr}, {"//div[id='single_book']//div[class='cover']//img", "src", nullptr} }) + .authors_handler("//div[id='single_book']//a[class='author']", "text", "href", "/author/") .list_chapters_handler("//div[class='chapters']//div[class='chapter']//a", "text", "href", "/manga/") .list_chapters_uploaded_time_handler("//div[class='chapters']//div[class='update_time']", "text", nullptr) .list_page_images_custom_handler([](const std::string &html_source) { @@ -1271,8 +1272,12 @@ namespace QuickMedia { std::string err_msg; if(matrix->did_initial_sync_fail(err_msg)) { show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); + matrix->logout(); + current_page = PageType::CHAT_LOGIN; + chat_login_page(); + after_matrix_login_page(); window.close(); - exit(0); + exit(exit_code); } } } @@ -1703,9 +1708,10 @@ namespace QuickMedia { FetchResult fetch_result = associated_data.fetch_future.get(); tabs[i].body->items = std::move(fetch_result.body_items); if(tabs[i].search_bar) tabs[i].body->filter_search_fuzzy(tabs[i].search_bar->get_text()); + LazyFetchPage *lazy_fetch_page = static_cast<LazyFetchPage*>(tabs[i].page.get()); if(fetch_result.result != PluginResult::OK) associated_data.search_result_text.setString("Failed to fetch page!"); - else if(tabs[i].body->items.empty()) + else if(tabs[i].body->items.empty() && !lazy_fetch_page->lazy_fetch_is_loader()) associated_data.search_result_text.setString("No results found"); else associated_data.search_result_text.setString(""); @@ -4024,7 +4030,12 @@ namespace QuickMedia { show_notification("QuickMedia", "Error: invalid command: " + text + ", expected /upload, /logout, /me or /react", Urgency::NORMAL); return false; } - } + } else if(chat_state == ChatState::REPLYING && text[0] == '/') { + if(strncmp(text.c_str(), "/react ", 7) == 0) { + msgtype = "m.reaction"; + text.erase(text.begin(), text.begin() + 7); + } + } auto message = std::make_shared<Message>(); message->user = matrix->get_me(current_room); @@ -4042,10 +4053,14 @@ namespace QuickMedia { if(tabs[MESSAGES_TAB_INDEX].body->is_selected_item_last_visible_item() && selected_tab == MESSAGES_TAB_INDEX) scroll_to_end = true; - if(chat_state == ChatState::TYPING_MESSAGE) { + if(chat_state == ChatState::TYPING_MESSAGE || (chat_state == ChatState::REPLYING && msgtype == "m.reaction")) { BodyItem *selected_item = tabs[MESSAGES_TAB_INDEX].body->get_selected(); + if(chat_state == ChatState::REPLYING) + selected_item = currently_operating_on_item.get(); if(msgtype == "m.reaction" && selected_item) { void *related_to_message = selected_item->userdata; + if(chat_state == ChatState::REPLYING) + related_to_message = currently_operating_on_item->userdata; message->type = MessageType::REACTION; message->related_event_type = RelatedEventType::REACTION; message->related_event_id = static_cast<Message*>(related_to_message)->event_id; @@ -4903,7 +4918,7 @@ namespace QuickMedia { chat_login_page(); if(current_page == PageType::CHAT) after_matrix_login_page(); - exit(0); + exit(exit_code); break; } default: @@ -4956,14 +4971,11 @@ namespace QuickMedia { if(redraw) { redraw = false; - float room_name_padding_y = 0.0f; - float padding_bottom = 0.0f; if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) { - room_name_padding_y = std::floor(10.0f * get_ui_scale() + room_name_total_height); tab_vertical_offset = std::floor(10.0f * get_ui_scale()); } - tab_shade_height = std::floor(tab_vertical_offset) + Tabs::get_height() + room_name_padding_y + padding_bottom; + tab_shade_height = std::floor(tab_vertical_offset) + Tabs::get_height() + room_name_padding_y; float body_padding_horizontal = 10.0f; float body_padding_vertical = std::floor(10.0f); @@ -5237,6 +5249,10 @@ namespace QuickMedia { std::string err_msg; if(matrix->did_initial_sync_fail(err_msg)) { show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); + matrix->logout(); + current_page = PageType::CHAT_LOGIN; + chat_login_page(); + after_matrix_login_page(); window.close(); goto chat_page_end; } @@ -5255,6 +5271,10 @@ namespace QuickMedia { std::this_thread::sleep_for(std::chrono::milliseconds(10)); if(matrix->did_initial_sync_fail(err_msg)) { show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); + matrix->logout(); + current_page = PageType::CHAT_LOGIN; + chat_login_page(); + after_matrix_login_page(); window.close(); goto chat_page_end; } @@ -5344,7 +5364,11 @@ namespace QuickMedia { MatrixQuickMedia matrix_handler(this, matrix, matrix_rooms_page.get(), matrix_rooms_tag_page.get(), matrix_invites_page.get()); bool sync_cached = false; - matrix->start_sync(&matrix_handler, sync_cached); + if(!matrix->start_sync(&matrix_handler, sync_cached)) { + show_notification("QuickMedia", "Failed to start sync", Urgency::CRITICAL); + exit_code = 1; + return; + } is_login_sync = !sync_cached; std::vector<Tab> tabs; diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp index 6aa6eec..059d028 100644 --- a/src/SearchBar.cpp +++ b/src/SearchBar.cpp @@ -55,11 +55,17 @@ namespace QuickMedia { } void SearchBar::draw(sf::RenderWindow &window, bool draw_shadow) { + sf::Vector2u window_size = window.getSize(); + if(window_size.x != prev_window_size.x || window_size.y != prev_window_size.y) { + needs_update = true; + prev_window_size = window_size; + } + if(needs_update) { needs_update = false; - sf::Vector2u window_size = window.getSize(); onWindowResize(sf::Vector2f(window_size.x, window_size.y)); } + (void)draw_shadow; //if(draw_shadow) // window.draw(background_shadow); diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index 111822e..56d746c 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -67,6 +67,11 @@ namespace QuickMedia { return str.substr(start, end - start + 1); } + bool string_starts_with(const std::string &str, const char *sub) { + size_t sub_len = strlen(sub); + return sub_len == 0 || (str.size() >= sub_len && memcmp(str.c_str(), sub, sub_len) == 0); + } + bool string_ends_with(const std::string &str, const std::string &ends_with_str) { size_t ends_len = ends_with_str.size(); return ends_len == 0 || (str.size() >= ends_len && memcmp(&str[str.size() - ends_len], ends_with_str.data(), ends_len) == 0); diff --git a/src/plugins/MangaGeneric.cpp b/src/plugins/MangaGeneric.cpp index 24472e5..58bedf4 100644 --- a/src/plugins/MangaGeneric.cpp +++ b/src/plugins/MangaGeneric.cpp @@ -49,11 +49,6 @@ namespace QuickMedia { return quickmedia_html_node_get_attribute_value(node, field_name); } - static bool starts_with(const std::string &str, const char *sub) { - size_t sub_len = strlen(sub); - return str.size() >= sub_len && memcmp(str.c_str(), sub, sub_len) == 0; - } - static int html_append_search(QuickMediaHtmlSearch *html_search, const char *html_query, HtmlSearchUserdata *search_userdata) { return quickmedia_html_find_nodes_xpath(html_search, html_query, [](QuickMediaHtmlNode *node, void *userdata) { @@ -149,12 +144,8 @@ namespace QuickMedia { return plugin_result_to_search_result(get_page(str, 0, result_items)); } - PluginResult MangaGenericSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { + PluginResult MangaGenericSearchPage::get_page(const std::string &url, BodyItems &result_items) { std::string target_url; - std::string url = search_query.search_template; - string_replace_all(url, "%s", url_param_encode(str)); - string_replace_all(url, "%p", std::to_string(search_query.page_start + page)); - std::string website_data; if(download_to_string(url, website_data, {}, true, fail_on_http_error) != DownloadResult::OK) return PluginResult::NET_ERR; @@ -184,36 +175,6 @@ namespace QuickMedia { if(result != 0) goto cleanup; - for(const DescriptionQuery &description_query : description_queries) { - assert(description_query.html_query && description_query.field_name); - if(description_query.html_query && description_query.field_name) { - HtmlMergeUserdata merge_userdata; - merge_userdata.type = MergeType::DESCRIPTION; - merge_userdata.body_item_image_context.body_items = &new_result_items; - merge_userdata.body_item_image_context.index = 0; - merge_userdata.field_name = description_query.field_name; - merge_userdata.field_contains = nullptr; - result = html_body_item_merge(&html_search, description_query.html_query, &merge_userdata); - if(result != 0) - goto cleanup; - } - } - - for(const ThumbnailQuery &thumbnail_query : thumbnail_queries) { - assert(thumbnail_query.html_query && thumbnail_query.field_name); - if(thumbnail_query.html_query && thumbnail_query.field_name) { - HtmlMergeUserdata merge_userdata; - merge_userdata.type = MergeType::THUMBNAIL; - merge_userdata.body_item_image_context.body_items = &new_result_items; - merge_userdata.body_item_image_context.index = 0; - merge_userdata.field_name = thumbnail_query.field_name; - merge_userdata.field_contains = thumbnail_query.field_contains; - result = html_body_item_merge(&html_search, thumbnail_query.html_query, &merge_userdata); - if(result != 0) - goto cleanup; - } - } - if(!text_query.url_field && !new_result_items.empty()) { if(target_url.empty()) { std::string response_headers; @@ -239,15 +200,45 @@ namespace QuickMedia { result_items.insert(result_items.end(), std::move_iterator(new_result_items.begin()), std::move_iterator(new_result_items.end())); } + for(const DescriptionQuery &description_query : description_queries) { + assert(description_query.html_query && description_query.field_name); + if(description_query.html_query && description_query.field_name) { + HtmlMergeUserdata merge_userdata; + merge_userdata.type = MergeType::DESCRIPTION; + merge_userdata.body_item_image_context.body_items = &result_items; + merge_userdata.body_item_image_context.index = 0; + merge_userdata.field_name = description_query.field_name; + merge_userdata.field_contains = nullptr; + result = html_body_item_merge(&html_search, description_query.html_query, &merge_userdata); + if(result != 0) + goto cleanup; + } + } + + for(const ThumbnailQuery &thumbnail_query : thumbnail_queries) { + assert(thumbnail_query.html_query && thumbnail_query.field_name); + if(thumbnail_query.html_query && thumbnail_query.field_name) { + HtmlMergeUserdata merge_userdata; + merge_userdata.type = MergeType::THUMBNAIL; + merge_userdata.body_item_image_context.body_items = &result_items; + merge_userdata.body_item_image_context.index = 0; + merge_userdata.field_name = thumbnail_query.field_name; + merge_userdata.field_contains = thumbnail_query.field_contains; + result = html_body_item_merge(&html_search, thumbnail_query.html_query, &merge_userdata); + if(result != 0) + goto cleanup; + } + } + for(auto &body_item : result_items) { - if(starts_with(body_item->url, "//")) + if(string_starts_with(body_item->url, "//")) body_item->url = "https://" + body_item->url.substr(2); - else if(starts_with(body_item->url, "/")) + else if(string_starts_with(body_item->url, "/")) body_item->url = website_url + body_item->url.substr(1); - if(starts_with(body_item->thumbnail_url, "//")) + if(string_starts_with(body_item->thumbnail_url, "//")) body_item->thumbnail_url = "https://" + body_item->thumbnail_url.substr(2); - else if(starts_with(body_item->thumbnail_url, "/")) + else if(string_starts_with(body_item->thumbnail_url, "/")) body_item->thumbnail_url = website_url + body_item->thumbnail_url.substr(1); } @@ -261,12 +252,20 @@ namespace QuickMedia { } } + PluginResult MangaGenericSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { + std::string url = search_query.search_template; + string_replace_all(url, "%s", url_param_encode(str)); + string_replace_all(url, "%p", std::to_string(search_query.page_start + page)); + return get_page(url, result_items); + } + PluginResult MangaGenericSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { if(!list_chapters_query.html_query || !list_chapters_query.title_field || !list_chapters_query.url_field) { assert(false); return PluginResult::ERR; } + std::vector<Creator> creators; BodyItems chapters_items; HtmlSearchUserdata search_userdata; search_userdata.body_items = &chapters_items; @@ -299,18 +298,49 @@ namespace QuickMedia { result = html_body_item_merge(&html_search, list_chapters_query.uploaded_time_html_query, &merge_userdata); } + if(authors_query.html_query && authors_query.title_field && authors_query.url_field) { + struct HtmlAuthorsUserdata { + std::vector<Creator> *creators; + AuthorsQuery *authors_query; + }; + + HtmlAuthorsUserdata authors_userdata; + authors_userdata.creators = &creators; + authors_userdata.authors_query = &authors_query; + + quickmedia_html_find_nodes_xpath(&html_search, authors_query.html_query, + [](QuickMediaHtmlNode *node, void *userdata) { + HtmlAuthorsUserdata *authors_userdata = (HtmlAuthorsUserdata*)userdata; + const char *title_value = html_attr_or_inner_text(node, authors_userdata->authors_query->title_field); + const char *url_value = html_attr_or_inner_text(node, authors_userdata->authors_query->url_field); + if(title_value && url_value && (!authors_userdata->authors_query->url_contains || strstr(url_value, authors_userdata->authors_query->url_contains))) { + Creator creator; + creator.name = strip(title_value); + creator.url = strip(url_value); + authors_userdata->creators->push_back(std::move(creator)); + } + }, &authors_userdata); + } + for(auto &body_item : chapters_items) { - if(starts_with(body_item->url, "//")) + if(string_starts_with(body_item->url, "//")) body_item->url = "https://" + body_item->url.substr(2); - else if(starts_with(body_item->url, "/")) + else if(string_starts_with(body_item->url, "/")) body_item->url = website_url + body_item->url.substr(1); - if(starts_with(body_item->thumbnail_url, "//")) + if(string_starts_with(body_item->thumbnail_url, "//")) body_item->thumbnail_url = "https://" + body_item->thumbnail_url.substr(2); - else if(starts_with(body_item->thumbnail_url, "/")) + else if(string_starts_with(body_item->thumbnail_url, "/")) body_item->thumbnail_url = website_url + body_item->thumbnail_url.substr(1); } + for(auto &creator : creators) { + if(string_starts_with(creator.url, "//")) + creator.url = "https://" + creator.url.substr(2); + else if(string_starts_with(creator.url, "/")) + creator.url = website_url + creator.url.substr(1); + } + cleanup: quickmedia_html_search_deinit(&html_search); if(result != 0) @@ -319,6 +349,11 @@ namespace QuickMedia { auto body = create_body(); body->items = std::move(chapters_items); result_tabs.push_back(Tab{std::move(body), std::make_unique<MangaGenericChaptersPage>(program, title, url, manga_id_extractor, service_name, website_url, &list_page_query, fail_on_http_error), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + for(Creator &creator : creators) { + result_tabs.push_back(Tab{create_body(), std::make_unique<MangaGenericCreatorPage>(program, this, std::move(creator)), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } + return PluginResult::OK; } @@ -348,6 +383,14 @@ namespace QuickMedia { return true; } + PluginResult MangaGenericCreatorPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + return search_page->submit(title, url, result_tabs); + } + + PluginResult MangaGenericCreatorPage::lazy_fetch(BodyItems &result_items) { + return search_page->get_page(creator.url, result_items); + } + static bool is_number(const char *str) { while(*str) { char c = *str; @@ -443,9 +486,9 @@ namespace QuickMedia { return ImageResult::ERR; } - if(starts_with(current_image_url, "//")) + if(string_starts_with(current_image_url, "//")) current_image_url = "https://" + current_image_url.substr(2); - else if(starts_with(current_image_url, "/")) + else if(string_starts_with(current_image_url, "/")) current_image_url = website_url + current_image_url.substr(1); num_images = page_count_userdata.num_pages; @@ -523,9 +566,9 @@ namespace QuickMedia { cleanup: quickmedia_html_search_deinit(&html_search); - if(starts_with(current_image_url, "//")) + if(string_starts_with(current_image_url, "//")) current_image_url = "https://" + current_image_url.substr(2); - else if(starts_with(current_image_url, "/")) + else if(string_starts_with(current_image_url, "/")) current_image_url = website_url + current_image_url.substr(1); if(!callback(current_image_url)) @@ -603,9 +646,9 @@ namespace QuickMedia { } for(std::string &url : chapter_image_urls) { - if(starts_with(url, "//")) + if(string_starts_with(url, "//")) url = "https://" + url.substr(2); - else if(starts_with(url, "/")) + else if(string_starts_with(url, "/")) url = website_url + url.substr(1); } @@ -635,6 +678,14 @@ namespace QuickMedia { return *this; } + MangaGenericSearchPage& MangaGenericSearchPage::authors_handler(const char *html_query, const char *title_field, const char *url_field, const char *url_contains) { + authors_query.html_query = html_query; + authors_query.title_field = title_field; + authors_query.url_field = url_field; + authors_query.url_contains = url_contains; + return *this; + } + MangaGenericSearchPage& MangaGenericSearchPage::list_chapters_handler(const char *html_query, const char *title_field, const char *url_field, const char *url_contains) { list_chapters_query.html_query = html_query; list_chapters_query.title_field = title_field; diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 57c879e..0c06458 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -489,14 +489,15 @@ namespace QuickMedia { } static std::string message_to_room_description_text(Message *message) { + std::string body = strip(message->body); if(message->type == MessageType::REACTION) - return "Reacted with: " + extract_first_line_elipses(message->body, 150); + return "Reacted with: " + extract_first_line_elipses(body, 150); else if(message->related_event_type == RelatedEventType::REPLY) - return extract_first_line_elipses(remove_reply_formatting(message->body), 150); + return extract_first_line_elipses(remove_reply_formatting(body), 150); else if(message->related_event_type == RelatedEventType::EDIT) - return "Edited: " + extract_first_line_elipses(remove_reply_formatting(message->body), 150); + return "Edited: " + extract_first_line_elipses(remove_reply_formatting(body), 150); else - return extract_first_line_elipses(message->body, 150); + return extract_first_line_elipses(body, 150); } void MatrixQuickMedia::update_room_description(RoomData *room, Messages &new_messages, bool is_initial_sync, bool sync_is_cache, Body *chat_body, bool messages_tab_visible) { @@ -1090,18 +1091,20 @@ namespace QuickMedia { } } - void Matrix::start_sync(MatrixDelegate *delegate, bool &cached) { + bool Matrix::start_sync(MatrixDelegate *delegate, bool &cached) { cached = true; if(sync_running) - return; + return true; assert(!this->delegate); assert(!access_token.empty()); // Need to be logged in this->delegate = delegate; Path matrix_cache_dir = get_cache_dir().join("matrix"); - if(create_directory_recursive(matrix_cache_dir) != 0) + if(create_directory_recursive(matrix_cache_dir) != 0) { fprintf(stderr, "Failed to create matrix cache directory\n"); + return false; + } matrix_cache_dir.join("sync_data.json"); cached = (get_file_type(matrix_cache_dir) == FileType::REGULAR); @@ -1196,6 +1199,7 @@ namespace QuickMedia { result = parse_sync_response(json_root, false, initial_sync); if(result != PluginResult::OK) { fprintf(stderr, "Failed to parse sync response\n"); + initial_sync = false; goto sync_end; } @@ -1206,6 +1210,7 @@ namespace QuickMedia { } else { //set_next_batch("Invalid"); fprintf(stderr, "Matrix: missing next batch\n"); + initial_sync = false; goto sync_end; } @@ -1288,6 +1293,8 @@ namespace QuickMedia { std::this_thread::sleep_for(std::chrono::milliseconds(500)); } }); + + return true; } void Matrix::stop_sync() { @@ -1313,6 +1320,10 @@ namespace QuickMedia { delegate = nullptr; sync_failed = false; sync_fail_reason.clear(); + set_next_batch(""); + invites.clear(); + filter_cached.reset(); + my_events_transaction_ids.clear(); } bool Matrix::is_initial_sync_finished() const { @@ -3382,7 +3393,7 @@ namespace QuickMedia { } PluginResult Matrix::logout() { - assert(!sync_running); + stop_sync(); Path session_path = get_storage_dir().join(SERVICE_NAME).join("session.json"); remove(session_path.data.c_str()); @@ -3402,10 +3413,6 @@ namespace QuickMedia { homeserver.clear(); homeserver_domain.clear(); upload_limit.reset(); - set_next_batch(""); - invites.clear(); - filter_cached.reset(); - my_events_transaction_ids.clear(); return PluginResult::OK; } @@ -3950,7 +3957,7 @@ namespace QuickMedia { // TODO: Use at param? which is room->get_prev_batch(); char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/members?membership=join", homeserver.c_str(), room->id.c_str()); + snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/members?not_membership=leave", homeserver.c_str(), room->id.c_str()); rapidjson::Document json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); diff --git a/src/plugins/Soundcloud.cpp b/src/plugins/Soundcloud.cpp index a0184b4..6740504 100644 --- a/src/plugins/Soundcloud.cpp +++ b/src/plugins/Soundcloud.cpp @@ -2,9 +2,11 @@ #include "../../include/NetUtils.hpp" #include "../../include/StringUtils.hpp" #include "../../include/Scale.hpp" +#include <quickmedia/HtmlSearch.h> +#include <thread> namespace QuickMedia { - static std::string client_id = "Na04L87fnpWDMVCCW2ngWldN4JMoLTAc"; + static std::string client_id; class SoundcloudPlaylist : public BodyItemExtra { public: @@ -218,6 +220,11 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult SoundcloudSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + submit_page.submit_body_item = submit_body_item; + return submit_page.submit(title, url, result_tabs); + } + SearchResult SoundcloudSearchPage::search(const std::string &str, BodyItems &result_items) { query_urn.clear(); PluginResult result = get_page(str, 0, result_items); @@ -268,6 +275,65 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult SoundcloudSearchPage::lazy_fetch(BodyItems&) { + std::string website_data; + DownloadResult download_result = download_to_string("https://soundcloud.com/", website_data, {}, true); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + if(client_id.empty()) { + std::vector<std::string> script_sources; + QuickMediaHtmlSearch html_search; + int result = quickmedia_html_search_init(&html_search, website_data.c_str()); + if(result != 0) + goto cleanup; + + result = quickmedia_html_find_nodes_xpath(&html_search, "//script", + [](QuickMediaHtmlNode *node, void *userdata) { + std::vector<std::string> *script_sources = (std::vector<std::string>*)userdata; + const char *src = quickmedia_html_node_get_attribute_value(node, "src"); + if(src) + script_sources->push_back(strip(src)); + }, &script_sources); + + cleanup: + quickmedia_html_search_deinit(&html_search); + if(result != 0) + return PluginResult::ERR; + + std::vector<std::future<std::string>> async_download_threads; + 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(std::async(std::launch::async, [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 ""; + + size_t index = website_data.find("web-auth?client_id="); + if(index == std::string::npos) return ""; + + index += 19; + size_t end = website_data.find('&', index); + if(end == std::string::npos) end = website_data.size(); + return website_data.substr(index, end - index); + })); + } + + for(auto &download_thread : async_download_threads) { + if(download_thread.valid()) { + std::string fetched_client_id = download_thread.get(); + if(client_id.empty() && !fetched_client_id.empty()) + client_id = std::move(fetched_client_id); + } + } + } + + return PluginResult::OK; + } + PluginResult SoundcloudUserPage::get_page(const std::string&, int page, BodyItems &result_items) { while(current_page < page) { PluginResult plugin_result = get_continuation_page(result_items); |