From 76ef34393aa72230a3490ecf7b06647ede1448da Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 9 Sep 2021 11:35:47 +0200 Subject: Add initial peertube support --- src/Config.cpp | 1 + src/QuickMedia.cpp | 35 +++- src/Theme.cpp | 1 + src/VideoPlayer.cpp | 1 + src/plugins/Peertube.cpp | 421 +++++++++++++++++++++++++++++++++++++++++++++++ src/plugins/Youtube.cpp | 4 +- 6 files changed, 456 insertions(+), 7 deletions(-) create mode 100644 src/plugins/Peertube.cpp (limited to 'src') 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 #include 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 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 ] [-e ] [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 Set the start directory when using file-manager. Default is the the users home directory\n"); + fprintf(stderr, " --instance The instance to use for peertube\n"); fprintf(stderr, " -e Embed QuickMedia into another window\n"); fprintf(stderr, "EXAMPLES:\n"); fprintf(stderr, " quickmedia\n"); @@ -324,6 +327,7 @@ namespace QuickMedia { std::vector 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 &tabs, int &start_tab_index, FileManagerMimeType fm_mime_type, FileSelectionHandler file_selection_handler) { + void Program::load_plugin_by_name(std::vector &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(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(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else { + tabs.push_back(Tab{create_body(false, true), std::make_unique(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(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 #include 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 &result_tabs) { + result_tabs.push_back(Tab{create_body(false, true), std::make_unique(program, url), create_search_bar("Search...", 350)}); + 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")); + 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 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 &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(program, server, title, url), nullptr }); + } else if(search_type == SearchType::VIDEO_PLAYLISTS) { + result_tabs.push_back(Tab{ create_body(false, true), std::make_unique(program, server, title, url), nullptr }); + } else if(search_type == SearchType::VIDEOS) { + result_tabs.push_back(Tab{ nullptr, std::make_unique(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 &result_tabs) { + result_tabs.push_back(Tab{ nullptr, std::make_unique(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 &result_tabs) { + result_tabs.push_back(Tab{ nullptr, std::make_unique(program, server, url, true), nullptr }); + return PluginResult::OK; + } + + PluginResult PeertubePlaylistPage::lazy_fetch(BodyItems &result_items) { + return get_page("", 0, result_items); + } + + std::unique_ptr PeertubeVideoPage::create_comments_page(Program*) { + return nullptr; + } + + std::unique_ptr PeertubeVideoPage::create_related_videos_page(Program*) { + return nullptr; + } + + std::unique_ptr 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 &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&, 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; -- cgit v1.2.3