#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(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; std::string name; const Json::Value &title_json = value_json["title"]; const Json::Value &name_json = result_json["name"]; if(title_json.isString()) name = title_json.asString(); else if(name_json.isString()) name = name_json.asString(); auto body_item = BodyItem::create(std::move(name)); 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"); } } 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 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 &resposted_claim_json = result_json["reposted_claim"]; if(resposted_claim_json.isObject()) return resolve_claim(resposted_claim_json, time_now); else return resolve_claim(result_json, time_now); } 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 SubmitArgs &args, std::vector &result_tabs) { if(args.userdata == search_type_video) result_tabs.push_back(Tab{ nullptr, std::make_unique(program, args.title, args.url), nullptr }); else if(args.userdata == search_type_channel) result_tabs.push_back(Tab{ create_body(false, true), std::make_unique(program, args.title, args.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 SubmitArgs &args, std::vector &result_tabs) { result_tabs.push_back(Tab{ nullptr, std::make_unique(program, args.title, args.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; } // 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); } }