#include "../../plugins/Peertube.hpp" #include "../../include/Theme.hpp" #include "../../include/Notification.hpp" #include "../../include/Utils.hpp" #include "../../include/StringUtils.hpp" #include namespace QuickMedia { static const char* search_type_to_string(PeertubeSearchPage::SearchType search_type) { switch(search_type) { case PeertubeSearchPage::SearchType::VIDEO_CHANNELS: return "video-channels"; case PeertubeSearchPage::SearchType::VIDEO_PLAYLISTS: return "video-playlists"; case PeertubeSearchPage::SearchType::VIDEOS: return "videos"; } return ""; } PluginResult PeertubeInstanceSelectionPage::submit(const SubmitArgs &args, std::vector &result_tabs) { result_tabs.push_back(Tab{create_body(false, true), std::make_unique(program, args.url), create_search_bar("Search...", 500)}); return PluginResult::OK; } static std::shared_ptr create_instance_selection_item(const std::string &title, const std::string &url) { auto body_item = BodyItem::create(title); body_item->url = url; return body_item; } PluginResult PeertubeInstanceSelectionPage::lazy_fetch(BodyItems &result_items) { result_items.push_back(create_instance_selection_item("tube.midov.pl", "https://tube.midov.pl")); result_items.push_back(create_instance_selection_item("videos.lukesmith.xyz", "https://videos.lukesmith.xyz")); result_items.push_back(create_instance_selection_item("peertube.se", "https://peertube.se")); result_items.push_back(create_instance_selection_item("bittube.video", "https://bittube.video")); result_items.push_back(create_instance_selection_item("video.nobodyhasthe.biz", "https://video.nobodyhasthe.biz")); result_items.push_back(create_instance_selection_item("libre.tube", "https://libre.tube")); result_items.push_back(create_instance_selection_item("open.tube", "https://open.tube")); result_items.push_back(create_instance_selection_item("runtube.re", "https://runtube.re")); result_items.push_back(create_instance_selection_item("tube.kenfm.de", "https://tube.kenfm.de")); result_items.push_back(create_instance_selection_item("tcode.kenfm.de", "https://tcode.kenfm.de")); result_items.push_back(create_instance_selection_item("tube.querdenken-711.de", "https://tube.querdenken-711.de")); result_items.push_back(create_instance_selection_item("peertube.rage.lol", "https://peertube.rage.lol")); result_items.push_back(create_instance_selection_item("gegenstimme.tv", "https://gegenstimme.tv")); result_items.push_back(create_instance_selection_item("tv.poa.st", "https://tv.poa.st")); result_items.push_back(create_instance_selection_item("libre.video", "https://libre.video")); result_items.push_back(create_instance_selection_item("gorf.tube", "https://gorf.tube")); result_items.push_back(create_instance_selection_item("neogenesis.tv", "https://neogenesis.tv")); return PluginResult::OK; } PeertubeSearchPage::PeertubeSearchPage(Program *program, const std::string &server_) : LazyFetchPage(program), server(server_) { if(!server.empty() && server.back() == '/') server.pop_back(); } SearchResult PeertubeSearchPage::search(const std::string &str, BodyItems &result_items) { return plugin_result_to_search_result(get_page(str, 0, result_items)); } // TODO: Support remote content static std::shared_ptr search_data_to_body_item(const Json::Value &data_json, const std::string &server, PeertubeSearchPage::SearchType search_type) { if(!data_json.isObject()) return nullptr; const Json::Value &name_json = data_json["name"]; const Json::Value &host_json = data_json["host"]; const Json::Value &display_name_json = data_json["displayName"]; const Json::Value &uuid_json = data_json["uuid"]; const Json::Value &short_uuid_json = data_json["shortUUID"]; std::string name_str; if(name_json.isString()) name_str = name_json.asString(); std::string display_name_str; if(display_name_json.isString()) display_name_str = display_name_json.asString(); else display_name_str = name_str; auto body_item = BodyItem::create(std::move(display_name_str)); body_item->userdata = (void*)search_type; if(search_type == PeertubeSearchPage::SearchType::VIDEO_PLAYLISTS) { if(uuid_json.isString()) body_item->url = uuid_json.asString(); } else { if(short_uuid_json.isString()) body_item->url = short_uuid_json.asString(); else if(name_json.isString() && host_json.isString()) body_item->url = name_str + "@" + host_json.asString(); else if(uuid_json.isString()) body_item->url = uuid_json.asString(); else return nullptr; } std::string description; const Json::Value &videos_length_json = data_json["videosLength"]; if(videos_length_json.isInt()) description += std::to_string(videos_length_json.asInt()) + " video" + (videos_length_json.asInt() == 1 ? "" : "s"); const Json::Value &views_json = data_json["views"]; if(views_json.isInt()) description += number_separate_thousand_commas(std::to_string(views_json.asInt())) + " view" + (views_json.asInt() == 1 ? "" : "s"); const Json::Value published_at_json = data_json["publishedAt"]; if(published_at_json.isString()) { if(!description.empty()) description += " • "; const time_t unix_time = iso_utc_to_unix_time(published_at_json.asCString()); description += "Published " + seconds_to_relative_time_str(time(nullptr) - unix_time); } const Json::Value updated_at_json = data_json["updatedAt"]; if(!published_at_json.isString() && updated_at_json.isString()) { if(!description.empty()) description += " • "; const time_t unix_time = iso_utc_to_unix_time(updated_at_json.asCString()); description += "Updated " + seconds_to_relative_time_str(time(nullptr) - unix_time); } const Json::Value &duration_json = data_json["duration"]; if(duration_json.isInt()) { if(!description.empty()) description += '\n'; description += seconds_to_duration(duration_json.asInt()); } for(const char *field_name : { "account", "videoChannel", "ownerAccount" }) { const Json::Value &account_json = data_json[field_name]; if(account_json.isObject()) { const Json::Value &channel_name_json = account_json["name"]; if(channel_name_json.isString()) { if(!description.empty()) description += '\n'; description += channel_name_json.asString(); const Json::Value &account_host_json = account_json["host"]; if(account_host_json.isString()) description += "@" + account_host_json.asString(); break; } } } if(!description.empty()) { body_item->set_description(std::move(description)); body_item->set_description_color(get_theme().faded_text_color); } const Json::Value &owner_account_json = data_json["ownerAccount"]; if(owner_account_json.isObject()) { const Json::Value &avatar_json = owner_account_json["avatar"]; if(avatar_json.isObject()) { const Json::Value &path_json = avatar_json["path"]; if(path_json.isString()) { body_item->thumbnail_url = server + path_json.asString(); body_item->thumbnail_size = { 130, 130 }; } } } const Json::Value &thumbnail_path_json = data_json["thumbnailPath"]; if(thumbnail_path_json.isString()) { body_item->thumbnail_url = server + thumbnail_path_json.asString(); body_item->thumbnail_size = { 280, 153 }; } if(search_type == PeertubeSearchPage::SearchType::VIDEO_CHANNELS && body_item->thumbnail_url.empty()) { body_item->thumbnail_url = server + "/client/assets/images/default-avatar-videochannel.png"; body_item->thumbnail_size = { 130, 130 }; } return body_item; } PluginResult PeertubeSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { if(str.empty()) return get_local_videos(page, result_items); // TODO: Parallel PluginResult result; result = get_page_by_type(SearchType::VIDEO_CHANNELS, str, page, 10, result_items); if(result != PluginResult::OK) return result; result = get_page_by_type(SearchType::VIDEO_PLAYLISTS, str, page, 10, result_items); if(result != PluginResult::OK) return result; result = get_page_by_type(SearchType::VIDEOS, str, page, 10, result_items); if(result != PluginResult::OK) return result; return PluginResult::OK; } // Returns true if the error was handled (if there was an error) static bool handle_error(const Json::Value &json_root, std::string &err_str) { if(!json_root.isObject()) return false; const Json::Value &status_json = json_root["status"]; const Json::Value &detail_json = json_root["detail"]; if(status_json.isInt() && detail_json.isString()) { err_str = detail_json.asString(); return true; } return false; } static PluginResult videos_request(Page *page, const std::string &url, const std::string &server, PeertubeSearchPage::SearchType search_type, BodyItems &result_items) { Json::Value json_root; std::string err_msg; DownloadResult result = page->download_json(json_root, url, {}, true, &err_msg); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); // Return OK because some versions of peertube doesn't support video-playlists api (or is it not enabled for the instance?) and in such cases it returns // an empty string if(!json_root.isObject()) return PluginResult::OK; std::string err_str; if(handle_error(json_root, err_str)) { show_notification("QuickMedia", "Peertube server returned an error: " + err_str, Urgency::CRITICAL); return PluginResult::ERR; } const Json::Value &data_array_json = json_root["data"]; if(data_array_json.isArray()) { for(const Json::Value &data_json : data_array_json) { const Json::Value &video_json = data_json["video"]; auto body_item = search_data_to_body_item(video_json.isObject() ? video_json : data_json, server, search_type); if(body_item) result_items.push_back(std::move(body_item)); } } return PluginResult::OK; } PluginResult PeertubeSearchPage::get_local_videos(int page, BodyItems &result_items) { char url[2048]; snprintf(url, sizeof(url), "%s/api/v1/videos/?start=%d&count=%d&sort=-publishedAt&filter=local&skipCount=true", server.c_str(), page * 20, 20); return videos_request(this, url, server, SearchType::VIDEOS, result_items); } PluginResult PeertubeSearchPage::get_page_by_type(SearchType search_type, const std::string &str, int page, int count, BodyItems &result_items) { char url[2048]; snprintf(url, sizeof(url), "%s/api/v1/search/%s?start=%d&count=%d&search=%s&searchTarget=local", server.c_str(), search_type_to_string(search_type), page * count, count, url_param_encode(str).c_str()); return videos_request(this, url, server, search_type, result_items); } PluginResult PeertubeSearchPage::submit(const SubmitArgs &args, std::vector &result_tabs) { const SearchType search_type = (SearchType)(uintptr_t)args.userdata; if(search_type == SearchType::VIDEO_CHANNELS) { result_tabs.push_back(Tab{ create_body(false, true), std::make_unique(program, server, args.title, args.url), nullptr }); } else if(search_type == SearchType::VIDEO_PLAYLISTS) { result_tabs.push_back(Tab{ create_body(false, true), std::make_unique(program, server, args.title, args.url), nullptr }); } else if(search_type == SearchType::VIDEOS) { result_tabs.push_back(Tab{ nullptr, std::make_unique(program, server, args.url, false), nullptr }); } return PluginResult::OK; } PluginResult PeertubeSearchPage::lazy_fetch(BodyItems &result_items) { return get_local_videos(0, result_items); } PluginResult PeertubeChannelPage::get_page(const std::string&, int page, BodyItems &result_items) { char url[2048]; snprintf(url, sizeof(url), "%s/api/v1/video-channels/%s/videos?start=%d&count=%d&sort=-publishedAt", server.c_str(), name.c_str(), page * 20, 20); return videos_request(this, url, server, PeertubeSearchPage::SearchType::VIDEOS, result_items); } PluginResult PeertubeChannelPage::submit(const SubmitArgs &args, std::vector &result_tabs) { result_tabs.push_back(Tab{ nullptr, std::make_unique(program, server, args.url, false), nullptr }); return PluginResult::OK; } PluginResult PeertubeChannelPage::lazy_fetch(BodyItems &result_items) { return get_page("", 0, result_items); } PluginResult PeertubePlaylistPage::get_page(const std::string&, int page, BodyItems &result_items) { char url[2048]; snprintf(url, sizeof(url), "%s/api/v1/video-playlists/%s/videos?start=%d&count=%d", server.c_str(), uuid.c_str(), page * 20, 20); return videos_request(this, url, server, PeertubeSearchPage::SearchType::VIDEOS, result_items); } PluginResult PeertubePlaylistPage::submit(const SubmitArgs &args, std::vector &result_tabs) { result_tabs.push_back(Tab{ nullptr, std::make_unique(program, server, args.url, true), nullptr }); return PluginResult::OK; } PluginResult PeertubePlaylistPage::lazy_fetch(BodyItems &result_items) { return get_page("", 0, result_items); } static std::string get_ext_from_url(const std::string &url) { const size_t dot_index = url.rfind('.'); if(dot_index == std::string::npos) return ""; return url.substr(dot_index); } std::string PeertubeVideoPage::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 PeertubeVideoPage::get_video_url(int max_height, bool &has_embedded_audio, std::string &ext) { has_embedded_audio = true; for(const PeertubeVideoPage::VideoSource &video_source : video_sources) { if(video_source.resolution <= max_height) { ext = get_ext_from_url(video_source.url); return video_source.url; } } if(!video_sources.empty()) { ext = get_ext_from_url(video_sources.front().url); return video_sources.front().url; } return ""; } std::string PeertubeVideoPage::get_audio_url(std::string&) { // TODO: Return when audio only mode is enabled return ""; } // TODO: Download video using torrent and seed it to at least 2x ratio static bool files_get_sources(const Json::Value &files_json, std::vector &video_sources) { if(!files_json.isArray()) return false; for(const Json::Value &file_json : files_json) { if(!file_json.isObject()) continue; const Json::Value &file_download_url_json = file_json["fileDownloadUrl"]; if(!file_download_url_json.isString()) continue; PeertubeVideoPage::VideoSource video_source; video_source.url = file_download_url_json.asString(); video_source.resolution = 0; const Json::Value &resolution_json = file_json["resolution"]; if(resolution_json.isObject()) { const Json::Value &id_json = resolution_json["id"]; if(id_json.isInt()) video_source.resolution = id_json.asInt(); } video_sources.push_back(std::move(video_source)); } // TODO: Also sort by fps std::sort(video_sources.begin(), video_sources.end(), [](const PeertubeVideoPage::VideoSource &source1, const PeertubeVideoPage::VideoSource &source2) { return source1.resolution > source2.resolution; }); return !video_sources.empty(); } // TODO: Media chapters PluginResult PeertubeVideoPage::load(std::string &title, std::string &channel_url, double &duration, std::vector&, std::string &err_str) { Json::Value json_root; std::string err_msg; DownloadResult download_result = download_json(json_root, server + "/api/v1/videos/" + url, {}, true, &err_msg); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); if(!json_root.isObject()) return PluginResult::ERR; if(handle_error(json_root, err_str)) return PluginResult::ERR; const Json::Value &name_json = json_root["name"]; if(name_json.isString()) title = name_json.asString(); const Json::Value &duration_json = json_root["duration"]; if(duration_json.isInt64()) duration = duration_json.asInt64(); const Json::Value &channel_json = json_root["channel"]; if(channel_json.isObject()) { const Json::Value &channel_url_json = channel_json["url"]; if(channel_url_json.isString()) channel_url = channel_url_json.asString(); } video_sources.clear(); if(!files_get_sources(json_root["files"], video_sources)) { const Json::Value &streaming_playlists_json = json_root["streamingPlaylists"]; if(!streaming_playlists_json.isArray()) return PluginResult::ERR; for(const Json::Value &streaming_playlist_json : streaming_playlists_json) { if(!streaming_playlist_json.isObject()) continue; #if 0 files_get_sources(streaming_playlist_json["files"], video_sources); #else const Json::Value &playlist_url_json = streaming_playlist_json["playlistUrl"]; if(!playlist_url_json.isString()) continue; PeertubeVideoPage::VideoSource video_source; video_source.url = playlist_url_json.asString(); video_source.resolution = 0; video_sources.push_back(std::move(video_source)); break; #endif } if(video_sources.empty()) return PluginResult::ERR; } return PluginResult::OK; } }