From 060db5c6cbd02e684a0c98c0f045da242b6ab218 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 9 Oct 2021 14:49:13 +0200 Subject: Add lbry, attempt to fix 4chan posting when captcha is no-op --- src/plugins/Lbry.cpp | 397 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 src/plugins/Lbry.cpp (limited to 'src/plugins/Lbry.cpp') 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 +#include + +// 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 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/ + // 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 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 &result_tabs) { + if(submit_body_item->userdata == search_type_video) + result_tabs.push_back(Tab{ nullptr, std::make_unique(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(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 &result_tabs) { + result_tabs.push_back(Tab{ nullptr, std::make_unique(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 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 LbryVideoPage::create_comments_page(Program*) { + return nullptr; + } + + std::unique_ptr LbryVideoPage::create_related_videos_page(Program*) { + return nullptr; + } + + std::unique_ptr 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&, 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 -- cgit v1.2.3