aboutsummaryrefslogtreecommitdiff
path: root/src
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
parentda2988c4356d2756e86037b1c7e859f49583c109 (diff)
Add initial peertube support
Diffstat (limited to 'src')
-rw-r--r--src/Config.cpp1
-rw-r--r--src/QuickMedia.cpp35
-rw-r--r--src/Theme.cpp1
-rw-r--r--src/VideoPlayer.cpp1
-rw-r--r--src/plugins/Peertube.cpp421
-rw-r--r--src/plugins/Youtube.cpp4
6 files changed, 456 insertions, 7 deletions
diff --git a/src/Config.cpp b/src/Config.cpp
index 31f8df9..c75f120 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -1,5 +1,6 @@
#include "../include/Config.hpp"
#include "../include/Storage.hpp"
+#include <json/value.h>
#include <assert.h>
namespace QuickMedia {
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 6b85d34..a038f66 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -5,6 +5,7 @@
#include "../plugins/MangaCombined.hpp"
#include "../plugins/MediaGeneric.hpp"
#include "../plugins/Youtube.hpp"
+#include "../plugins/Peertube.hpp"
#include "../plugins/Fourchan.hpp"
#include "../plugins/NyaaSi.hpp"
#include "../plugins/Matrix.hpp"
@@ -72,6 +73,7 @@ static const std::pair<const char*, const char*> valid_plugins[] = {
std::make_pair("readm", "readm_logo.png"),
std::make_pair("manga", nullptr),
std::make_pair("youtube", "yt_logo_rgb_dark_small.png"),
+ std::make_pair("peertube", "peertube_logo.png"),
std::make_pair("soundcloud", "soundcloud_logo.png"),
std::make_pair("pornhub", "pornhub_logo.png"),
std::make_pair("spankbang", "spankbang_logo.png"),
@@ -281,11 +283,12 @@ namespace QuickMedia {
static void usage() {
fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--dir <directory>] [-e <window>] [youtube-url]\n");
fprintf(stderr, "OPTIONS:\n");
- fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n");
+ fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, peertube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n");
fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n");
fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n");
fprintf(stderr, " --upscale-images-always Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default\n");
fprintf(stderr, " --dir <directory> Set the start directory when using file-manager. Default is the the users home directory\n");
+ fprintf(stderr, " --instance <instance> The instance to use for peertube\n");
fprintf(stderr, " -e <window> Embed QuickMedia into another window\n");
fprintf(stderr, "EXAMPLES:\n");
fprintf(stderr, " quickmedia\n");
@@ -324,6 +327,7 @@ namespace QuickMedia {
std::vector<Tab> tabs;
const char *url = nullptr;
std::string program_path = dirname(argv[0]);
+ std::string instance;
for(int i = 1; i < argc; ++i) {
if(!plugin_name) {
@@ -348,6 +352,15 @@ namespace QuickMedia {
upscale_image_action = UpscaleImageAction::LOW_RESOLUTION;
} else if(strcmp(argv[i], "--upscale-images-force") == 0 || strcmp(argv[i], "--upscale-images-always") == 0) {
upscale_image_action = UpscaleImageAction::FORCE;
+ } else if(strcmp(argv[i], "--instance") == 0) {
+ if(i < argc - 1) {
+ instance = argv[i + 1];
+ ++i;
+ } else {
+ fprintf(stderr, "Missing instance after --instance argument\n");
+ usage();
+ return -1;
+ }
} else if(strcmp(argv[i], "--dir") == 0) {
if(i < argc - 1) {
file_manager_start_dir = argv[i + 1];
@@ -475,7 +488,7 @@ namespace QuickMedia {
file_selection_handler = std::move(saucenao_file_selection_handler);
}
- load_plugin_by_name(tabs, start_tab_index, fm_mine_type, std::move(file_selection_handler));
+ load_plugin_by_name(tabs, start_tab_index, fm_mine_type, std::move(file_selection_handler), std::move(instance));
while(!tabs.empty() || matrix) {
if(matrix) {
@@ -500,7 +513,7 @@ namespace QuickMedia {
fm_mine_type = FILE_MANAGER_MIME_TYPE_IMAGE;
file_selection_handler = std::move(saucenao_file_selection_handler);
}
- load_plugin_by_name(tabs, start_tab_index, fm_mine_type, std::move(file_selection_handler));
+ load_plugin_by_name(tabs, start_tab_index, fm_mine_type, std::move(file_selection_handler), "");
}
}
@@ -986,7 +999,7 @@ namespace QuickMedia {
}
}
- void Program::load_plugin_by_name(std::vector<Tab> &tabs, int &start_tab_index, FileManagerMimeType fm_mime_type, FileSelectionHandler file_selection_handler) {
+ void Program::load_plugin_by_name(std::vector<Tab> &tabs, int &start_tab_index, FileManagerMimeType fm_mime_type, FileSelectionHandler file_selection_handler, std::string instance) {
if(!plugin_name || plugin_name[0] == '\0')
return;
@@ -1027,6 +1040,7 @@ namespace QuickMedia {
create_launcher_body_item("Readm", "readm", resources_root + "icons/readm_launcher.png"),
create_launcher_body_item("Matrix", "matrix", resources_root + "icons/matrix_launcher.png"),
create_launcher_body_item("Nyaa.si", "nyaa.si", resources_root + "icons/nyaa_si_launcher.png"),
+ create_launcher_body_item("PeerTube", "peertube", resources_root + "images/peertube_logo.png"),
create_launcher_body_item("SauceNAO", "saucenao", ""),
create_launcher_body_item("Soundcloud", "soundcloud", resources_root + "icons/soundcloud_launcher.png"),
create_launcher_body_item("YouTube", "youtube", resources_root + "icons/yt_launcher.png"),
@@ -1195,6 +1209,12 @@ namespace QuickMedia {
auto youtube_video_page = std::make_unique<YoutubeVideoPage>(this, youtube_url);
video_content_page(nullptr, youtube_video_page.get(), "", false, nullptr, 0);
}
+ } else if(strcmp(plugin_name, "peertube") == 0) {
+ if(instance.empty()) {
+ tabs.push_back(Tab{create_body(false, false), std::make_unique<PeertubeInstanceSelectionPage>(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ } else {
+ tabs.push_back(Tab{create_body(false, true), std::make_unique<PeertubeSearchPage>(this, instance), create_search_bar("Search...", 350)});
+ }
} else if(strcmp(plugin_name, "pornhub") == 0) {
check_youtube_dl_installed(plugin_name);
auto search_page = std::make_unique<MediaGenericSearchPage>(this, "https://www.pornhub.com/", sf::Vector2i(320/1.5f, 180/1.5f), false);
@@ -1739,6 +1759,8 @@ namespace QuickMedia {
bool redraw = true;
for(Tab &tab : tabs) {
+ assert(tab.body.get());
+ assert(tab.page.get());
if(tab.body->attach_side == AttachSide::BOTTOM)
tab.body->select_last_item();
tab.page->on_navigate_to_page(tab.body.get());
@@ -2429,11 +2451,14 @@ namespace QuickMedia {
unsigned long num_items = 0;
unsigned long bytes_after = 0;
unsigned char *properties = nullptr;
- if(XGetWindowProperty(display, window, wm_state_atom, 0, 1024, False, XA_ATOM, &type, &format, &num_items, &bytes_after, &properties) < Success || !properties) {
+ if(XGetWindowProperty(display, window, wm_state_atom, 0, 1024, False, XA_ATOM, &type, &format, &num_items, &bytes_after, &properties) < Success) {
fprintf(stderr, "Failed to get window wm state property\n");
return false;
}
+ if(!properties)
+ return false;
+
bool is_fullscreen = false;
Atom *atoms = (Atom*)properties;
for(unsigned long i = 0; i < num_items; ++i) {
diff --git a/src/Theme.cpp b/src/Theme.cpp
index 8f52240..c0702d1 100644
--- a/src/Theme.cpp
+++ b/src/Theme.cpp
@@ -1,6 +1,7 @@
#include "../include/Theme.hpp"
#include "../include/Config.hpp"
#include "../include/Storage.hpp"
+#include <json/value.h>
#include <assert.h>
namespace QuickMedia {
diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp
index 04bc27f..047e525 100644
--- a/src/VideoPlayer.cpp
+++ b/src/VideoPlayer.cpp
@@ -155,6 +155,7 @@ namespace QuickMedia {
"--sub-font-size=40",
"--sub-margin-y=45",
"--sub-border-size=1.95",
+ //"--force_all_formats=no",
cache_dir.c_str(),
input_conf.c_str(),
wid_arg.c_str()
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
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index b6dc2f7..c5a6d04 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -2007,9 +2007,9 @@ namespace QuickMedia {
int hours = 0;
int minutes = 0;
int seconds = 0;
- if(sscanf(timestamp.c_str(), "%dh%dm%d", &hours, &minutes, &seconds) == 3)
+ if(sscanf(timestamp.c_str(), "%dh%dm%ds", &hours, &minutes, &seconds) == 3)
return (hours * 60 * 60) + (minutes * 60) + seconds;
- if(sscanf(timestamp.c_str(), "%dm%d", &minutes, &seconds) == 2)
+ if(sscanf(timestamp.c_str(), "%dm%ds", &minutes, &seconds) == 2)
return (minutes * 60) + seconds;
if(sscanf(timestamp.c_str(), "%d", &seconds) == 1)
return seconds;