diff options
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | TODO | 4 | ||||
-rw-r--r-- | icons/lbry_launcher.png | bin | 0 -> 3243 bytes | |||
-rw-r--r-- | icons/xhamster_launcher.png | bin | 2731 -> 0 bytes | |||
-rw-r--r-- | icons/xvideos_launcher.png | bin | 2843 -> 0 bytes | |||
-rw-r--r-- | images/lbry_logo.png | bin | 0 -> 6284 bytes | |||
-rw-r--r-- | images/yt_launcher.png | bin | 1752 -> 0 bytes | |||
-rw-r--r-- | include/StringUtils.hpp | 1 | ||||
-rwxr-xr-x | install.sh | 1 | ||||
-rw-r--r-- | plugins/Fourchan.hpp | 1 | ||||
-rw-r--r-- | plugins/ImageBoard.hpp | 2 | ||||
-rw-r--r-- | plugins/Lbry.hpp | 53 | ||||
-rw-r--r-- | plugins/NyaaSi.hpp | 1 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 7 | ||||
-rw-r--r-- | src/StringUtils.cpp | 22 | ||||
-rw-r--r-- | src/VideoPlayer.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Fourchan.cpp | 2 | ||||
-rw-r--r-- | src/plugins/ImageBoard.cpp | 5 | ||||
-rw-r--r-- | src/plugins/Lbry.cpp | 397 | ||||
-rw-r--r-- | src/plugins/Manganelo.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Peertube.cpp | 22 | ||||
-rw-r--r-- | src/plugins/Soundcloud.cpp | 32 |
22 files changed, 499 insertions, 62 deletions
@@ -1,13 +1,13 @@ # QuickMedia A rofi inspired native client for web services. -Currently supported web services: `youtube`, `peertube`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples`, `anilist` and _others_.\ +Currently supported web services: `youtube`, `peertube`, `lbry`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples`, `anilist` and _others_.\ Config data, including manga progress is stored under `$XDG_CONFIG_HOME/quickmedia` or `$HOME/.config/quickmedia`.\ Cache is stored under `$XDG_CACHE_HOME/quickmedia` or `$HOME/.cache/quickmedia`. ## Usage ``` usage: quickmedia [plugin] [--dir <directory>] [-e <window>] [youtube-url] OPTIONS: - plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, peertube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager or stdin + plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager or stdin --no-video Only play audio when playing a video. Disabled by default --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default --upscale-images-always Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default @@ -153,7 +153,8 @@ Type text and then wait and QuickMedia will automatically search.\ `/me [text]`: Send a message of type "m.emote".\ `/react [text]`: React to the selected message (also works if you are replying to a message). ## Config -Config is loaded from `$HOME/quickmedia/config.json` if it exists. See [example-config.json](https://git.dec05eba.com/QuickMedia/plain/example-config.json) for an example config. All fields in the config file are optional. +Config is loaded from `$HOME/quickmedia/config.json` if it exists. See [example-config.json](https://git.dec05eba.com/QuickMedia/plain/example-config.json) for an example config. All fields in the config file are optional.\ +If `use_system_mpv_config` is set to `true` then your systems mpv config in `~/.config/mpv/mpv.conf` and plugins will be used. If you have a mpv plugin installed that uses `input-ipc-server` (such as `mpv-discord`) then it will break quickmedia integration with mpv (especially key inputs such as ctrl+r). ## Theme Theme is loaded from `$HOME/quickmedia/themes/<theme-name>.json` if it exists or from `/usr/share/quickmedia/themes`. Theme name is set in `$HOME/quickmedia/config.json` under the variable `theme`.\ Default themes available: `default, nord`.\ @@ -198,4 +198,6 @@ Peertube urls should play directly in quickmedia. Test peertube with live streams. Peertube hls streams can be really slow to start up (especially for videos.autizmo.xyz). This is an issue in ffmpeg. Maybe use youtube proxy downloader for this? those videos are fragmented. Add keybindings for image control for 4chan. -Fix youtube videos that are age restricted AND do not allow embedding. Is that even possible?
\ No newline at end of file +Fix youtube videos that are age restricted AND do not allow embedding. Is that even possible? +Use local lbry instead of odysee when I figure out the incorrect instructions of to use lighthouse locally. +ffmpeg (and mpv) is very slow at playing streams (mostly affects lbry and certain peertube videos) for some reason.
\ No newline at end of file diff --git a/icons/lbry_launcher.png b/icons/lbry_launcher.png Binary files differnew file mode 100644 index 0000000..f20735a --- /dev/null +++ b/icons/lbry_launcher.png diff --git a/icons/xhamster_launcher.png b/icons/xhamster_launcher.png Binary files differdeleted file mode 100644 index 4c6b8a4..0000000 --- a/icons/xhamster_launcher.png +++ /dev/null diff --git a/icons/xvideos_launcher.png b/icons/xvideos_launcher.png Binary files differdeleted file mode 100644 index d279cab..0000000 --- a/icons/xvideos_launcher.png +++ /dev/null diff --git a/images/lbry_logo.png b/images/lbry_logo.png Binary files differnew file mode 100644 index 0000000..787b42a --- /dev/null +++ b/images/lbry_logo.png diff --git a/images/yt_launcher.png b/images/yt_launcher.png Binary files differdeleted file mode 100644 index 88085df..0000000 --- a/images/yt_launcher.png +++ /dev/null diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp index 29befc7..f03ac62 100644 --- a/include/StringUtils.hpp +++ b/include/StringUtils.hpp @@ -27,4 +27,5 @@ namespace QuickMedia { // Note: does not check for overflow bool to_num_hex(const char *str, size_t size, int &num); std::string seconds_to_relative_time_str(time_t seconds); + std::string seconds_to_duration(int seconds); }
\ No newline at end of file @@ -10,6 +10,7 @@ cd "$script_dir" sibs build --release install -Dm755 "sibs-build/$(sibs platform)/release/quickmedia" "/usr/bin/quickmedia" +ln -sf "/usr/bin/quickmedia" "/usr/bin/qm" install -Dm644 boards.json "/usr/share/quickmedia/boards.json" install -Dm644 input.conf "/usr/share/quickmedia/input.conf" diff --git a/plugins/Fourchan.hpp b/plugins/Fourchan.hpp index f09c7bb..8c608a1 100644 --- a/plugins/Fourchan.hpp +++ b/plugins/Fourchan.hpp @@ -8,7 +8,6 @@ namespace QuickMedia { FourchanBoardsPage(Program *program, std::string resources_root) : Page(program), resources_root(std::move(resources_root)) {} const char* get_title() const override { return "Select board"; } PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; - bool clear_search_after_submit() override { return true; } void get_boards(BodyItems &result_items); const std::string resources_root; diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp index 5b299b3..7914ef1 100644 --- a/plugins/ImageBoard.hpp +++ b/plugins/ImageBoard.hpp @@ -29,6 +29,8 @@ namespace QuickMedia { const char* get_title() const override { return ""; } PageTypez get_type() const override { return PageTypez::IMAGE_BOARD_THREAD; } + void copy_to_clipboard(const BodyItem *body_item) const override; + bool autoplay_next_item() override { return true; } std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program) override; std::unique_ptr<Page> create_channels_page(Program*, const std::string&) override { diff --git a/plugins/Lbry.hpp b/plugins/Lbry.hpp new file mode 100644 index 0000000..ac7c4b4 --- /dev/null +++ b/plugins/Lbry.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "Page.hpp" + +namespace QuickMedia { + class LbrySearchPage : public Page { + public: + LbrySearchPage(Program *program, std::string channel_id = "") : Page(program), channel_id(std::move(channel_id)) {} + const char* get_title() const override { return "Search"; } + bool search_is_filter() override { return false; } + SearchResult search(const std::string &str, BodyItems &result_items) override; + 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; + bool submit_is_async() const override { return false; } + private: + std::string channel_id; + }; + + class LbryChannelPage : public LazyFetchPage { + public: + LbryChannelPage(Program *program, std::string title, std::string channel_id) : + LazyFetchPage(program), search_page(program, channel_id), title(std::move(title)), channel_id(std::move(channel_id)) {} + const char* get_title() const override { return title.c_str(); } + bool submit_is_async() const override { return false; } + bool search_is_filter() override { return false; } + SearchResult search(const std::string &str, BodyItems &result_items) override; + 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; + PluginResult lazy_fetch(BodyItems &result_items) override; + private: + LbrySearchPage search_page; + std::string title; + std::string channel_id; + }; + + class LbryVideoPage : public VideoPage { + public: + LbryVideoPage(Program *program, std::string title, std::string url) : VideoPage(program, std::move(url)), title(std::move(title)) {} + const char* get_title() const override { return ""; } + //BodyItems get_related_media(const std::string &url) override; + //bool create_search_page(Program *program, Tab &tab) override; + std::unique_ptr<Page> create_comments_page(Program *program) override; + std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program) override; + std::unique_ptr<Page> create_channels_page(Program *program, const std::string &channel_url) override; + std::string get_download_url(int max_height) override; + std::string get_video_url(int max_height, bool &has_embedded_audio, std::string &ext) override; + std::string get_audio_url(std::string &ext) override; + PluginResult load(std::string &title, std::string &channel_url, std::vector<MediaChapter> &chapters, std::string &err_str) override; + private: + std::string title; + std::string streaming_url; + }; +}
\ No newline at end of file diff --git a/plugins/NyaaSi.hpp b/plugins/NyaaSi.hpp index f113544..5556fa8 100644 --- a/plugins/NyaaSi.hpp +++ b/plugins/NyaaSi.hpp @@ -11,7 +11,6 @@ namespace QuickMedia { 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<Tab> &result_tabs) override; - bool clear_search_after_submit() override { return true; } const bool is_sukebei; }; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 435ec3b..65d6159 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -10,6 +10,7 @@ #include "../plugins/NyaaSi.hpp" #include "../plugins/Matrix.hpp" #include "../plugins/Soundcloud.hpp" +#include "../plugins/Lbry.hpp" #include "../plugins/FileManager.hpp" #include "../plugins/Pipe.hpp" #include "../plugins/Saucenao.hpp" @@ -78,6 +79,7 @@ static const std::pair<const char*, const char*> valid_plugins[] = { std::make_pair("youtube", "yt_logo_rgb_dark_small.png"), std::make_pair("peertube", "peertube_logo.png"), std::make_pair("soundcloud", "soundcloud_logo.png"), + std::make_pair("lbry", "lbry_logo.png"), std::make_pair("pornhub", "pornhub_logo.png"), std::make_pair("spankbang", "spankbang_logo.png"), std::make_pair("xvideos", "xvideos_logo.png"), @@ -286,7 +288,7 @@ namespace QuickMedia { static void usage() { fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--dir <directory>] [-e <window>] [youtube-url]\n"); fprintf(stderr, "OPTIONS:\n"); - fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, peertube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n"); + fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n"); fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n"); fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n"); fprintf(stderr, " --upscale-images-always Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default\n"); @@ -1046,6 +1048,7 @@ namespace QuickMedia { create_launcher_body_item("4chan", "4chan", resources_root + "icons/4chan_launcher.png"), create_launcher_body_item("AniList", "anilist", resources_root + "images/anilist_logo.png"), create_launcher_body_item("Hot Examples", "hotexamples", ""), + create_launcher_body_item("Lbry", "lbry", "icons/lbry_launcher.png"), create_launcher_body_item("Manga (all)", "manga", ""), create_launcher_body_item("Mangadex", "mangadex", resources_root + "icons/mangadex_launcher.png"), create_launcher_body_item("Mangakatana", "mangakatana", resources_root + "icons/mangakatana_launcher.png"), @@ -1262,6 +1265,8 @@ namespace QuickMedia { } else if(strcmp(plugin_name, "soundcloud") == 0) { tabs.push_back(Tab{create_body(false, true), std::make_unique<SoundcloudSearchPage>(this), create_search_bar("Search...", 500)}); no_video = true; + } else if(strcmp(plugin_name, "lbry") == 0) { + tabs.push_back(Tab{create_body(false, true), std::make_unique<LbrySearchPage>(this), create_search_bar("Search...", 500)}); } else if(strcmp(plugin_name, "matrix") == 0) { assert(!matrix); if(create_directory_recursive(get_cache_dir().join("matrix").join("events")) != 0) { diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index 5706499..494e32f 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -219,4 +219,26 @@ namespace QuickMedia { else return std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s") + " ago"; } + + std::string seconds_to_duration(int seconds) { + seconds = std::max(0, seconds); + + int minutes = seconds / 60; + int hours = minutes / 60; + char buffer[32]; + + if(hours >= 1) { + minutes -= (hours * 60); + seconds -= (hours * 60 * 60); + seconds -= (minutes * 60); + snprintf(buffer, sizeof(buffer), "%02d:%02d:%02d", hours, minutes, seconds); + } else if(minutes >= 1) { + seconds -= (minutes * 60); + snprintf(buffer, sizeof(buffer), "%02d:%02d", minutes, seconds); + } else { + snprintf(buffer, sizeof(buffer), "00:%02d", seconds); + } + + return buffer; + } }
\ No newline at end of file diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp index 047e525..5d2bf3c 100644 --- a/src/VideoPlayer.cpp +++ b/src/VideoPlayer.cpp @@ -229,7 +229,7 @@ namespace QuickMedia { strcpy(ipc_addr.sun_path, ipc_server_path); int flags = fcntl(ipc_socket, F_GETFL, 0); - if(flags != -1) // TODO: Proper error handling + if(flags != -1) fcntl(ipc_socket, F_SETFL, flags | O_NONBLOCK); if(exec_program_async(args.data(), &video_process_id) != 0) { diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index f7c9910..01c8546 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -526,7 +526,7 @@ namespace QuickMedia { additional_args.push_back({ "--form-string", "filename=" + file_get_filename(filepath) }); } - if(pass_id.empty()) { + if(pass_id.empty() && !captcha_id.empty()) { additional_args.push_back(CommandArg{"--form-string", "t-challenge=" + captcha_id}); additional_args.push_back(CommandArg{"--form-string", "t-response=" + captcha_solution}); } diff --git a/src/plugins/ImageBoard.cpp b/src/plugins/ImageBoard.cpp index a2ffca7..f813068 100644 --- a/src/plugins/ImageBoard.cpp +++ b/src/plugins/ImageBoard.cpp @@ -1,6 +1,11 @@ #include "../../plugins/ImageBoard.hpp" +#include <SFML/Window/Clipboard.hpp> namespace QuickMedia { + void ImageBoardThreadPage::copy_to_clipboard(const BodyItem *body_item) const { + sf::Clipboard::setString(sf::String::fromUtf8(body_item->get_description().begin(), body_item->get_description().end())); + } + std::unique_ptr<RelatedVideosPage> ImageBoardThreadPage::create_related_videos_page(Program*) { return nullptr; } diff --git a/src/plugins/Lbry.cpp b/src/plugins/Lbry.cpp new file mode 100644 index 0000000..c35e430 --- /dev/null +++ b/src/plugins/Lbry.cpp @@ -0,0 +1,397 @@ +#include "../../plugins/Lbry.hpp" +#include "../../include/Notification.hpp" +#include "../../include/StringUtils.hpp" +#include "../../include/Theme.hpp" +#include <json/value.h> +#include <json/writer.h> + +// TODO: Images, music, regular files + +namespace QuickMedia { + static void *search_type_video = (void*)0; + static void *search_type_channel = (void*)1; + + static bool handle_error(const Json::Value &json_root, std::string &err_str) { + const Json::Value &error_json = json_root["error"]; + if(!error_json.isObject()) + return false; + + const Json::Value &code_json = error_json["code"]; + const Json::Value &message_json = error_json["message"]; + if(message_json.isString()) + err_str += "message: " + message_json.asString(); + if(code_json.isString()) + err_str += " (code: " + code_json.asString() + ")"; + return true; + } + + static std::shared_ptr<BodyItem> resolve_claim_parse_result(const Json::Value &result_json, time_t time_now) { + if(!result_json.isObject()) + return nullptr; + + const Json::Value &canonical_url_json = result_json["canonical_url"]; + const Json::Value &claim_id_json = result_json["claim_id"]; + const Json::Value &value_type_json = result_json["value_type"]; + if(!canonical_url_json.isString() || !claim_id_json.isString() || !value_type_json.isString()) + return nullptr; + + const Json::Value &value_json = result_json["value"]; + if(!value_json.isObject()) + return nullptr; + + const Json::Value &title_json = value_json["title"]; + if(!title_json.isString()) + return nullptr; + + auto body_item = BodyItem::create(title_json.asString()); + + bool is_channel = false; + // TODO: Support other types than stream and channel + if(strcmp(value_type_json.asCString(), "channel") == 0) { + body_item->url = claim_id_json.asString(); + body_item->userdata = search_type_channel; + is_channel = true; + } else if(strcmp(value_type_json.asCString(), "stream") == 0) { + body_item->url = canonical_url_json.asString(); + body_item->userdata = search_type_video; + + // Skip livestreams for now as they are pretty broken on lbry. + // Livestream requests work by doing GET https://api.live.odysee.com/v1/odysee/live/<claim_id> + // then get stream url with .data.url. If that is missing then there is no livestream going on. What to do then? + // TODO: Add livestreams when lbry fixes them. + const Json::Value &stream_type_json = value_json["stream_type"]; + if(!stream_type_json.isString() || strcmp(stream_type_json.asCString(), "video") != 0) + return nullptr; + } + body_item->thumbnail_size = { 177, 100 }; + + const Json::Value &thumbnail_json = value_json["thumbnail"]; + if(thumbnail_json.isObject()) { + const Json::Value &url_json = thumbnail_json["url"]; + if(url_json.isString()) { + if(strstr(url_json.asCString(), "ytimg.com")) + body_item->thumbnail_url = url_json.asString(); + else + body_item->thumbnail_url = url_json.asString() + "?quality=85&width=177&height=100"; + } + } + + std::string description; + + if(is_channel) { + const Json::Value &meta_json = result_json["meta"]; + if(meta_json.isObject()) { + const Json::Value &claims_in_channel_json = meta_json["claims_in_channel"]; + if(claims_in_channel_json.isInt()) { + const int claims_in_channel = claims_in_channel_json.asInt(); + description = std::to_string(claims_in_channel) + " upload" + (claims_in_channel == 0 ? "" : "s"); + } + } + + const Json::Value &name_json = result_json["name"]; + if(name_json.isString()) { + if(!description.empty()) + description += '\n'; + description += name_json.asString(); + } + } else { + const Json::Value ×tamp_json = result_json["timestamp"]; + if(timestamp_json.isInt64()) + description = seconds_to_relative_time_str(time_now - timestamp_json.asInt64()); + } + + const Json::Value &video_json = value_json["video"]; + if(video_json.isObject()) { + const Json::Value duration_json = video_json["duration"]; + if(duration_json.isInt()) { + if(!description.empty()) + description += '\n'; + description += seconds_to_duration(duration_json.asInt()); + } + } + + const Json::Value &signing_channel_json = result_json["signing_channel"]; + if(signing_channel_json.isObject()) { + const Json::Value &name_json = signing_channel_json["name"]; + if(name_json.isString()) { + if(!description.empty()) + description += '\n'; + description += name_json.asString(); + } + } + + if(!description.empty()) { + body_item->set_description(std::move(description)); + body_item->set_description_color(get_theme().faded_text_color); + } + + return body_item; + } + + static PluginResult resolve_claims(Page *page, const Json::Value &request_json, BodyItems &result_items) { + if(!request_json.isObject()) + return PluginResult::ERR; + + const Json::Value &method_json = request_json["method"]; + if(!method_json.isString()) + return PluginResult::ERR; + + std::string url = "https://api.na-backend.odysee.com/api/v1/proxy?m=" + method_json.asString(); + + Json::StreamWriterBuilder json_builder; + json_builder["commentStyle"] = "None"; + json_builder["indentation"] = ""; + + std::vector<CommandArg> additional_args = { + { "-X", "POST" }, + { "-H", "content-type: application/json" }, + { "--data-binary", Json::writeString(json_builder, request_json) } + }; + + Json::Value json_root; + DownloadResult download_result = page->download_json(json_root, url, std::move(additional_args), true); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + if(!json_root.isObject()) + return PluginResult::ERR; + + std::string err_str; + if(handle_error(json_root, err_str)) { + show_notification("QuickMedia", "Lbry search failed, error: " + err_str, Urgency::CRITICAL); + return PluginResult::ERR; + } + + const Json::Value &result_json = json_root["result"]; + if(!result_json.isObject()) + return PluginResult::ERR; + + const time_t time_now = time(nullptr); + const Json::Value &items_json = result_json["items"]; + if(items_json.isArray()) { + // Channel search + for(const Json::Value &result_json : items_json) { + auto body_item = resolve_claim_parse_result(result_json, time_now); + if(body_item) + result_items.push_back(std::move(body_item)); + } + } else { + // Global search + for(Json::Value::const_iterator it = result_json.begin(); it != result_json.end(); ++it) { + auto body_item = resolve_claim_parse_result(*it, time_now); + if(body_item) + result_items.push_back(std::move(body_item)); + } + } + + return PluginResult::OK; + } + + SearchResult LbrySearchPage::search(const std::string &str, BodyItems &result_items) { + return plugin_result_to_search_result(get_page(str, 0, result_items)); + } + + PluginResult LbrySearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { + if(str.empty()) + return PluginResult::OK; + + // TODO: Support other types than stream and channel + std::string url = "https://lighthouse.odysee.com/search?s=" + url_param_encode(str) + "&size=20&from=" + std::to_string(page * 20) + "&nsfw=false&claimType=stream,channel"; + if(!channel_id.empty()) + url += "&channel_id=" + channel_id; + + Json::Value json_root; + DownloadResult download_result = download_json(json_root, url, {}, true); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + if(!json_root.isArray()) + return PluginResult::ERR; + + Json::Value request_json(Json::objectValue); + request_json["id"] = (int64_t)time(nullptr) * 1000; + request_json["jsonrpc"] = "2.0"; + request_json["method"] = "resolve"; + + Json::Value request_params_json(Json::objectValue); + request_params_json["include_purchase_receipt"] = true; + + Json::Value urls_json(Json::arrayValue); + + for(const Json::Value &claim_json : json_root) { + if(!claim_json.isObject()) + continue; + + const Json::Value &claim_id_json = claim_json["claimId"]; + const Json::Value &name_json = claim_json["name"]; + if(!claim_id_json.isString() || !name_json.isString()) + continue; + + urls_json.append("lbry://" + name_json.asString() + "#" + claim_id_json.asString()); + } + + request_params_json["urls"] = std::move(urls_json); + request_json["params"] = std::move(request_params_json); + return resolve_claims(this, request_json, result_items); + } + + PluginResult LbrySearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + if(submit_body_item->userdata == search_type_video) + result_tabs.push_back(Tab{ nullptr, std::make_unique<LbryVideoPage>(program, title, url), nullptr }); + else if(submit_body_item->userdata == search_type_channel) + result_tabs.push_back(Tab{ create_body(false, true), std::make_unique<LbryChannelPage>(program, title, url), create_search_bar("Search...", 500) }); + return PluginResult::OK; + } + + SearchResult LbryChannelPage::search(const std::string &str, BodyItems &result_items) { + return plugin_result_to_search_result(get_page(str, 0, result_items)); + } + + PluginResult LbryChannelPage::get_page(const std::string &str, int page, BodyItems &result_items) { + if(!str.empty()) + return search_page.get_page(str, page, result_items); + + const int64_t time_now = time(nullptr); + + Json::Value channel_ids_json(Json::arrayValue); + channel_ids_json.append(channel_id); + + Json::Value claim_type_json(Json::arrayValue); + claim_type_json.append("stream"); + claim_type_json.append("repost"); + + Json::Value not_tags_json(Json::arrayValue); + not_tags_json.append("porn"); + not_tags_json.append("porno"); + not_tags_json.append("nsfw"); + not_tags_json.append("mature"); + not_tags_json.append("xxx"); + not_tags_json.append("sex"); + not_tags_json.append("creampie"); + not_tags_json.append("blowjob"); + not_tags_json.append("handjob"); + not_tags_json.append("vagina"); + not_tags_json.append("boobs"); + not_tags_json.append("big boobs"); + not_tags_json.append("big dick"); + not_tags_json.append("pussy"); + not_tags_json.append("cumshot"); + not_tags_json.append("anal"); + not_tags_json.append("hard fucking"); + not_tags_json.append("ass"); + not_tags_json.append("fuck"); + not_tags_json.append("hentai"); + + Json::Value order_by_json(Json::arrayValue); + order_by_json.append("release_time"); + + Json::Value request_params_json(Json::objectValue); + request_params_json["channel_ids"] = std::move(channel_ids_json); + request_params_json["claim_type"] = std::move(claim_type_json); + request_params_json["fee_amount"] = ">=0"; + request_params_json["has_source"] = true; + request_params_json["include_purchase_receipt"] = true; + request_params_json["no_totals"] = true; + request_params_json["not_tags"] = std::move(not_tags_json); + request_params_json["order_by"] = std::move(order_by_json); + request_params_json["page"] = 1 + page; + request_params_json["page_size"] = 20; + request_params_json["release_time"] = "<" + std::to_string(time_now); + + Json::Value request_json(Json::objectValue); + request_json["id"] = time_now * 1000; + request_json["jsonrpc"] = "2.0"; + request_json["method"] = "claim_search"; + request_json["params"] = std::move(request_params_json); + return resolve_claims(this, request_json, result_items); + } + + PluginResult LbryChannelPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + result_tabs.push_back(Tab{ nullptr, std::make_unique<LbryVideoPage>(program, title, url), nullptr }); + return PluginResult::OK; + } + + PluginResult LbryChannelPage::lazy_fetch(BodyItems &result_items) { + return get_page("", 0, result_items); + } + + static PluginResult video_get_stream_url(Page *page, const std::string &video_url, std::string &streaming_url, std::string &err_str) { + std::string url = "https://api.na-backend.odysee.com/api/v1/proxy?m=resolve"; + + Json::Value request_params_json(Json::objectValue); + request_params_json["save_file"] = false; + request_params_json["uri"] = video_url; + + Json::Value request_json(Json::objectValue); + request_json["id"] = (int64_t)time(nullptr) * 1000; + request_json["jsonrpc"] = "2.0"; + request_json["method"] = "get"; + request_json["params"] = std::move(request_params_json); + + Json::StreamWriterBuilder json_builder; + json_builder["commentStyle"] = "None"; + json_builder["indentation"] = ""; + + std::vector<CommandArg> additional_args = { + { "-X", "POST" }, + { "-H", "content-type: application/json" }, + { "--data-binary", Json::writeString(json_builder, request_json) } + }; + + Json::Value json_root; + DownloadResult download_result = page->download_json(json_root, url, std::move(additional_args), true); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + if(!json_root.isObject()) + return PluginResult::ERR; + + err_str.clear(); + if(handle_error(json_root, err_str)) + return PluginResult::ERR; + + const Json::Value &result_json = json_root["result"]; + if(!result_json.isObject()) + return PluginResult::ERR; + + const Json::Value &streaming_url_json = result_json["streaming_url"]; + if(!streaming_url_json.isString()) + return PluginResult::ERR; + + streaming_url = streaming_url_json.asString(); + return PluginResult::OK; + } + + std::unique_ptr<Page> LbryVideoPage::create_comments_page(Program*) { + return nullptr; + } + + std::unique_ptr<RelatedVideosPage> LbryVideoPage::create_related_videos_page(Program*) { + return nullptr; + } + + std::unique_ptr<Page> LbryVideoPage::create_channels_page(Program*, const std::string&) { + return nullptr; + } + + // TODO: Support |max_height|. This can be done by gettin video source hash and checking for sd_hash and then resolution. + // If max_height is below max resolution height then choose the sd_hash version (replace hash in video stream with sd hash for the lower quality version) + std::string LbryVideoPage::get_download_url(int max_height) { + bool has_embedded_audio; + std::string ext; + return get_video_url(max_height, has_embedded_audio, ext); + } + + std::string LbryVideoPage::get_video_url(int max_height, bool &has_embedded_audio, std::string &ext) { + has_embedded_audio = true; + ext = ".mp4"; // TODO: Check if this is always correct + return streaming_url; + } + + std::string LbryVideoPage::get_audio_url(std::string&) { + return ""; + } + + PluginResult LbryVideoPage::load(std::string &title, std::string&, std::vector<MediaChapter>&, std::string &err_str) { + streaming_url.clear(); + title = this->title; + return video_get_stream_url(this, url, streaming_url, err_str); + } +}
\ No newline at end of file diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index df86207..b1e61c4 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -109,7 +109,7 @@ namespace QuickMedia { std::string url = "https://manganelo.com/getstorysearchjson"; std::string search_term = "searchword="; search_term += url_param_encode(str); - CommandArg data_arg = { "--data", std::move(search_term) }; + CommandArg data_arg = { "--form-string", std::move(search_term) }; Json::Value json_root; DownloadResult result = download_json(json_root, url, {data_arg}, true); diff --git a/src/plugins/Peertube.cpp b/src/plugins/Peertube.cpp index 3b74871..b83daf5 100644 --- a/src/plugins/Peertube.cpp +++ b/src/plugins/Peertube.cpp @@ -56,28 +56,6 @@ namespace QuickMedia { return plugin_result_to_search_result(get_page(str, 0, result_items)); } - static std::string seconds_to_duration(int seconds) { - seconds = std::max(0, seconds); - - int minutes = seconds / 60; - int hours = minutes / 60; - char buffer[32]; - - if(hours >= 1) { - minutes -= (hours * 60); - seconds -= (hours * 60 * 60); - seconds -= (minutes * 60); - snprintf(buffer, sizeof(buffer), "%02d:%02d:%02d", hours, minutes, seconds); - } else if(minutes >= 1) { - seconds -= (minutes * 60); - snprintf(buffer, sizeof(buffer), "%02d:%02d", minutes, seconds); - } else { - snprintf(buffer, sizeof(buffer), "0:%02d", seconds); - } - - return buffer; - } - // TODO: Support remote content static std::shared_ptr<BodyItem> search_data_to_body_item(const Json::Value &data_json, const std::string &server, PeertubeSearchPage::SearchType search_type) { if(!data_json.isObject()) diff --git a/src/plugins/Soundcloud.cpp b/src/plugins/Soundcloud.cpp index d54060d..8885dfb 100644 --- a/src/plugins/Soundcloud.cpp +++ b/src/plugins/Soundcloud.cpp @@ -27,42 +27,14 @@ namespace QuickMedia { return ""; } - static std::string duration_to_descriptive_string(int64_t milliseconds) { - time_t seconds = milliseconds / 1000; - time_t minutes = seconds / 60; - time_t hours = minutes / 60; - - std::string str; - if(hours >= 1) { - str = std::to_string(hours) + " hour" + (hours == 1 ? "" : "s"); - minutes -= (hours * 60); - seconds -= (hours * 60 * 60); - } - - if(minutes >= 1) { - if(!str.empty()) - str += ", "; - str += std::to_string(minutes) + " minute" + (minutes == 1 ? "" : "s"); - seconds -= (minutes * 60); - } - - if(!str.empty() || seconds > 0) { - if(!str.empty()) - str += ", "; - str += std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s"); - } - - return str; - } - static std::string collection_item_get_duration(const Json::Value &item_json) { const Json::Value &full_duration_json = item_json["full_duration"]; if(full_duration_json.isInt64()) - return duration_to_descriptive_string(full_duration_json.asInt64()); + return seconds_to_duration(full_duration_json.asInt64()); const Json::Value &duration_json = item_json["duration"]; if(duration_json.isInt64()) - return duration_to_descriptive_string(duration_json.asInt64()); + return seconds_to_duration(duration_json.asInt64()); return ""; } |