aboutsummaryrefslogtreecommitdiff
path: root/src/plugins/Peertube.cpp
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2021-09-09 11:35:47 +0200
committerdec05eba <dec05eba@protonmail.com>2021-09-09 11:35:47 +0200
commit76ef34393aa72230a3490ecf7b06647ede1448da (patch)
tree1121b209293e719271b62c4fddeb4ec44ea6aac6 /src/plugins/Peertube.cpp
parentda2988c4356d2756e86037b1c7e859f49583c109 (diff)
Add initial peertube support
Diffstat (limited to 'src/plugins/Peertube.cpp')
-rw-r--r--src/plugins/Peertube.cpp421
1 files changed, 421 insertions, 0 deletions
diff --git a/src/plugins/Peertube.cpp b/src/plugins/Peertube.cpp
new file mode 100644
index 0000000..dacd2d0
--- /dev/null
+++ b/src/plugins/Peertube.cpp
@@ -0,0 +1,421 @@
+#include "../../plugins/Peertube.hpp"
+#include "../../include/Theme.hpp"
+#include "../../include/Notification.hpp"
+#include "../../include/Utils.hpp"
+#include "../../include/StringUtils.hpp"
+
+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 std::string&, const std::string &url, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{create_body(false, true), std::make_unique<PeertubeSearchPage>(program, url), create_search_bar("Search...", 350)});
+ return PluginResult::OK;
+ }
+
+ static std::shared_ptr<BodyItem> 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"));
+ 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));
+ }
+
+ 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);
+ 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())
+ 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
+ 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 += 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);
+
+ if(!json_root.isObject())
+ return PluginResult::ERR;
+
+ 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 std::string &title, const std::string &url, std::vector<Tab> &result_tabs) {
+ const SearchType search_type = (SearchType)(uintptr_t)submit_body_item->userdata;
+ if(search_type == SearchType::VIDEO_CHANNELS) {
+ result_tabs.push_back(Tab{ create_body(false, true), std::make_unique<PeertubeChannelPage>(program, server, title, url), nullptr });
+ } else if(search_type == SearchType::VIDEO_PLAYLISTS) {
+ result_tabs.push_back(Tab{ create_body(false, true), std::make_unique<PeertubePlaylistPage>(program, server, title, url), nullptr });
+ } else if(search_type == SearchType::VIDEOS) {
+ result_tabs.push_back(Tab{ nullptr, std::make_unique<PeertubeVideoPage>(program, server, 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 std::string&, const std::string &url, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{ nullptr, std::make_unique<PeertubeVideoPage>(program, server, 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 std::string&, const std::string &url, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{ nullptr, std::make_unique<PeertubeVideoPage>(program, server, url, true), nullptr });
+ return PluginResult::OK;
+ }
+
+ PluginResult PeertubePlaylistPage::lazy_fetch(BodyItems &result_items) {
+ return get_page("", 0, result_items);
+ }
+
+ std::unique_ptr<Page> PeertubeVideoPage::create_comments_page(Program*) {
+ return nullptr;
+ }
+
+ std::unique_ptr<RelatedVideosPage> PeertubeVideoPage::create_related_videos_page(Program*) {
+ return nullptr;
+ }
+
+ std::unique_ptr<Page> PeertubeVideoPage::create_channels_page(Program*, const std::string&) {
+ return nullptr;
+ }
+
+ 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_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<PeertubeVideoPage::VideoSource> &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, std::vector<MediaChapter>&, 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 &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;
+ files_get_sources(streaming_playlist_json["files"], video_sources);
+ }
+
+ if(video_sources.empty())
+ return PluginResult::ERR;
+ }
+
+ return PluginResult::OK;
+ }
+} \ No newline at end of file