aboutsummaryrefslogtreecommitdiff
path: root/src/plugins/Lbry.cpp
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2021-10-09 14:49:13 +0200
committerdec05eba <dec05eba@protonmail.com>2021-10-13 03:43:19 +0200
commit060db5c6cbd02e684a0c98c0f045da242b6ab218 (patch)
treef380f5eb398a3aa83f5c2c413c041137503c9589 /src/plugins/Lbry.cpp
parente2a510e8c94aae88aab6293659b1a0786c799bc2 (diff)
Add lbry, attempt to fix 4chan posting when captcha is no-op
Diffstat (limited to 'src/plugins/Lbry.cpp')
-rw-r--r--src/plugins/Lbry.cpp397
1 files changed, 397 insertions, 0 deletions
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 &timestamp_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