aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md5
-rw-r--r--TODO8
-rw-r--r--images/peertube_logo.pngbin0 -> 1311 bytes
-rw-r--r--include/QuickMedia.hpp2
-rw-r--r--include/Storage.hpp5
-rw-r--r--plugins/Page.hpp3
-rw-r--r--plugins/Peertube.hpp92
-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
13 files changed, 565 insertions, 13 deletions
diff --git a/README.md b/README.md
index 2d56af1..c1aa4be 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,18 @@
# QuickMedia
A rofi inspired native client for web services.
-Currently supported web services: `youtube`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples`, `anilist` and _others_.\
+Currently supported web services: `youtube`, `peertube`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples`, `anilist` and _others_.\
Config data, including manga progress is stored under `$XDG_CONFIG_HOME/quickmedia` or `$HOME/.config/quickmedia`.\
Cache is stored under `$XDG_CACHE_HOME/quickmedia` or `$HOME/.cache/quickmedia`.
## Usage
```
usage: quickmedia [plugin] [--dir <directory>] [-e <window>] [youtube-url]
OPTIONS:
- 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 or stdin
+ 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 or stdin
--no-video Only play audio when playing a video. Disabled by default
--upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default
--upscale-images-always Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default
--dir <directory> Set the start directory when using file-manager
+ --instance <instance> The instance to use for peertube
-e <window> Embed QuickMedia into another window
EXAMPLES:
quickmedia
diff --git a/TODO b/TODO
index b057e5d..26eee9c 100644
--- a/TODO
+++ b/TODO
@@ -190,4 +190,10 @@ Renable throttle detection after fixing it (it doesn't detect throttling well an
Show who deleted a message in matrix.
Sync should replace all messages in the room (except for the selected room?) to reduce ram usage when in many rooms and when quickmedia has been running for a long time doing sync.
Show youtube annotations.
-Show indicator in body item if it has been bookmarked (to prevent accidental removal of an item that has already been bookmarked before). \ No newline at end of file
+Show indicator in body item if it has been bookmarked (to prevent accidental removal of an item that has already been bookmarked before).
+Add option to bookmark peertube instance.
+Add ctrl+r menus for peertube.
+Add audio-only mode for peertube (without also downloading video).
+Add option to play peertube video directly from url, along with timestamp. Should also work for playlists.
+Peertube urls should play directly in quickmedia.
+Test peertube with live streams. \ No newline at end of file
diff --git a/images/peertube_logo.png b/images/peertube_logo.png
new file mode 100644
index 0000000..790fe33
--- /dev/null
+++ b/images/peertube_logo.png
Binary files differ
diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp
index 86a2993..db3dd09 100644
--- a/include/QuickMedia.hpp
+++ b/include/QuickMedia.hpp
@@ -103,7 +103,7 @@ namespace QuickMedia {
Json::Value load_video_history_json();
private:
void init(Window parent_window, std::string &program_path);
- void load_plugin_by_name(std::vector<Tab> &tabs, int &start_tab_index, FileManagerMimeType fm_mime_type, FileSelectionHandler file_selection_handler);
+ void load_plugin_by_name(std::vector<Tab> &tabs, int &start_tab_index, FileManagerMimeType fm_mime_type, FileSelectionHandler file_selection_handler, std::string instance);
void common_event_handler(sf::Event &event);
void handle_x11_events();
void base_event_handler(sf::Event &event, PageType previous_page, Body *body, SearchBar *search_bar, bool handle_key_press = true, bool handle_searchbar = true);
diff --git a/include/Storage.hpp b/include/Storage.hpp
index c3f40aa..9107bb2 100644
--- a/include/Storage.hpp
+++ b/include/Storage.hpp
@@ -3,9 +3,12 @@
#include "Path.hpp"
#include <functional>
#include <filesystem>
-#include <json/value.h>
#include <rapidjson/fwd.h>
+namespace Json {
+ class Value;
+}
+
namespace QuickMedia {
// Return false to stop the iterator
using FileIteratorCallback = std::function<bool(const std::filesystem::path &filepath)>;
diff --git a/plugins/Page.hpp b/plugins/Page.hpp
index 6944afb..071351b 100644
--- a/plugins/Page.hpp
+++ b/plugins/Page.hpp
@@ -135,7 +135,8 @@ namespace QuickMedia {
virtual std::unique_ptr<Page> create_channels_page(Program *program, const std::string &channel_url) = 0;
virtual void set_url(std::string new_url) { url = std::move(new_url); }
std::string get_url() { return url; }
- // Returns empty string for no timestamp or if the video doesn't support timestamps
+ // Returns empty string for no timestamp or if the video doesn't support timestamps.
+ // Timestamp is in seconds.
virtual std::string get_url_timestamp() { return ""; }
// Falls back to |get_url| if this and |get_audio_url| returns empty strings.
// Might do a network request.
diff --git a/plugins/Peertube.hpp b/plugins/Peertube.hpp
new file mode 100644
index 0000000..833cb5f
--- /dev/null
+++ b/plugins/Peertube.hpp
@@ -0,0 +1,92 @@
+#pragma once
+
+#include "Page.hpp"
+
+namespace QuickMedia {
+ class PeertubeInstanceSelectionPage : public LazyFetchPage {
+ public:
+ PeertubeInstanceSelectionPage(Program *program) : LazyFetchPage(program) {}
+ const char* get_title() const override { return "Select instance"; }
+ bool allow_submit_no_selection() const override { return true; }
+ PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ };
+
+ class PeertubeSearchPage : public LazyFetchPage {
+ public:
+ enum class SearchType {
+ VIDEO_CHANNELS,
+ VIDEO_PLAYLISTS,
+ VIDEOS
+ };
+
+ PeertubeSearchPage(Program *program, const std::string &server);
+ const char* get_title() const override { return "Search"; }
+ bool search_is_filter() override { return false; }
+ // Fetches local videos if |str| is empty
+ SearchResult search(const std::string &str, BodyItems &result_items) override;
+ PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
+ PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ // Fetches all local videos
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ private:
+ PluginResult get_local_videos(int page, BodyItems &result_items);
+ PluginResult get_page_by_type(SearchType search_type, const std::string &str, int page, int count, BodyItems &result_items);
+ private:
+ std::string server;
+ };
+
+ class PeertubeChannelPage : public LazyFetchPage {
+ public:
+ PeertubeChannelPage(Program *program, const std::string &server, std::string display_name, std::string name) :
+ LazyFetchPage(program), server(server), name(std::move(name)), title(std::move(display_name)) {}
+ const char* get_title() const override { return title.c_str(); }
+ PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
+ PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ private:
+ std::string server;
+ std::string name;
+ std::string title;
+ };
+
+ class PeertubePlaylistPage : public LazyFetchPage {
+ public:
+ PeertubePlaylistPage(Program *program, const std::string &server, std::string display_name, std::string uuid) :
+ LazyFetchPage(program), server(server), uuid(std::move(uuid)), title(std::move(display_name)) {}
+ const char* get_title() const override { return title.c_str(); }
+ PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
+ PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ private:
+ std::string server;
+ std::string uuid;
+ std::string title;
+ };
+
+ class PeertubeVideoPage : public VideoPage {
+ public:
+ struct VideoSource {
+ std::string url;
+ int resolution; // 720p = 720. 0 = no resolution found
+ };
+
+ PeertubeVideoPage(Program *program, std::string server, std::string url, bool autoplay_next) : VideoPage(program, std::move(url)), server(server), autoplay_next(autoplay_next) {}
+ const char* get_title() const override { return ""; }
+ //BodyItems get_related_media(const std::string &url) override;
+ //bool create_search_page(Program *program, Tab &tab) override;
+ std::unique_ptr<Page> create_comments_page(Program *program) override;
+ std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *program) override;
+ std::unique_ptr<Page> create_channels_page(Program *program, const std::string &channel_url) override;
+ std::string get_video_url(int max_height, bool &has_embedded_audio, std::string &ext) override;
+ std::string get_audio_url(std::string &ext) override;
+ PluginResult load(std::string &title, std::string &channel_url, std::vector<MediaChapter> &chapters, std::string &err_str) override;
+ bool autoplay_next_item() override { return autoplay_next; }
+ //void mark_watched() override;
+ //void get_subtitles(SubtitleData &subtitle_data) override;
+ private:
+ std::string server;
+ std::vector<VideoSource> video_sources;
+ bool autoplay_next;
+ };
+}
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;