From 9c1d43e772efb8f5af4b7ef5562fb433c8985697 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 29 Oct 2022 21:33:18 +0200 Subject: Youtube: allow opening youtube channels directly from url. Same with ctrl+i info menu --- README.md | 2 +- TODO | 5 +- include/QuickMedia.hpp | 1 + plugins/Youtube.hpp | 3 +- src/QuickMedia.cpp | 25 ++++++---- src/plugins/Info.cpp | 11 ++++- src/plugins/Matrix.cpp | 2 +- src/plugins/Youtube.cpp | 118 +++++++++++++++++++++++++++++++++--------------- 8 files changed, 117 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 39f8083..898a3ea 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Currently supported web services: `youtube`, `peertube`, `lbry`, `soundcloud`, ` QuickMedia also supports reading local manga and watching local anime, see [local manga](#local-manga) and [local anime](#local-anime) ## Usage ``` -usage: quickmedia [plugin] [--dir ] [-e ] [youtube-url] +usage: quickmedia [plugin] [--no-video] [--upscale-images] [--upscale-images-always] [--dir ] [--instance ] [-e ] [--video-max-height ] [youtube-url] [youtube-channel-url] OPTIONS: plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, dramacool, file-manager or stdin --no-video Only play audio when playing a video. Disabled by default diff --git a/TODO b/TODO index d2294e9..906525d 100644 --- a/TODO +++ b/TODO @@ -193,7 +193,6 @@ ffmpeg (and mpv) is very slow at playing streams (mostly affects lbry and certai Allow specifying start/end range for video/music downloads. Limit text input length for 4chan posts to the server limit. Allow creating a new thread on 4chan. -Support directly going to a youtube channel for a url. This is helpful for opening channel urls directly with quickmedia and also going to another channel from a youtube description. Support downloading soundcloud/youtube playlists. Such downloads should also have a different download gui as you would select a folder instead of an output file. Support downloading .m3u8 files, such as soundcloud music without using youtube-dl. Fix lbry and peertube download which fail because for lbry all videos are .m3u8 and some peertube videos are .m3u8. @@ -242,4 +241,6 @@ Update room name, avatar, etc in gui when updated. Automatically cleanup old cache files. Download manga pages in parallel. This helps downloading for certain websites such as mangakatana where a single page can take more than 2 seconds but loading 5 at once allows each page to load in 0.4 seconds. Allow pasting a file link (with or without file://) directly into matrix chat to upload a file (if the chat input is empty). This allows replying-with-media to work with ctrl+v. -Matrix image reply to image reply to text reply is a bit broken in the text formatting. \ No newline at end of file +Matrix image reply to image reply to text reply is a bit broken in the text formatting. +The formatting of replying to a message with an image in matrix is a bit weird. The reply image should be below the replied to message instead of on the left side. +Add ctrl+h to go back to the front page. \ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 8dca09e..d2ca1f3 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -227,6 +227,7 @@ namespace QuickMedia { std::string pipe_selected_text; std::filesystem::path file_manager_start_dir; std::string youtube_url; + std::string youtube_channel_url; std::unique_ptr video_player; bool use_youtube_dl = false; int video_max_height = 0; diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index 1e84af1..6ce62e8 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -35,6 +35,7 @@ namespace QuickMedia { // Returns |url| if the url is already a youtube url std::string invidious_url_to_youtube_url(const std::string &url); bool youtube_url_extract_id(const std::string &youtube_url, std::string &youtube_video_id); + bool youtube_url_extract_channel_id(const std::string &youtube_url, std::string &channel_id, std::string &channel_url); // |video_url| or |audio_url| will be empty if there is an error and false will be returned. // If false is returned from |active_handler|, then this function is cancelled. bool youtube_custom_redirect(std::string &video_url, std::string &audio_url, int64_t &video_content_length, int64_t &audio_content_length, std::function active_handler); @@ -113,7 +114,7 @@ namespace QuickMedia { private: PluginResult search_get_continuation(const std::string &url, const std::string &continuation_token, BodyItems &result_items); private: - const std::string url; + std::string url; std::string continuation_token; const std::string title; int current_page = 0; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 13931d2..69ac66e 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -318,7 +318,7 @@ namespace QuickMedia { } static void usage() { - fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--dir ] [-e ] [youtube-url]\n"); + fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--upscale-images] [--upscale-images-always] [--dir ] [--instance ] [-e ] [--video-max-height ] [youtube-url] [youtube-channel-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, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, dramacool, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n"); fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n"); @@ -387,11 +387,17 @@ namespace QuickMedia { for(int i = 1; i < argc; ++i) { if(!plugin_name) { - std::string youtube_video_id_dummy; std::string youtube_url_converted = invidious_url_to_youtube_url(argv[i]); - if(youtube_url_extract_id(youtube_url_converted, youtube_video_id_dummy)) { + std::string youtube_channel_id; + std::string youtube_video_id_dummy; + + if(youtube_url_extract_channel_id(youtube_url_converted, youtube_channel_id, youtube_channel_url)) { + plugin_name = "youtube"; + continue; + } else if(youtube_url_extract_id(youtube_url_converted, youtube_video_id_dummy)) { youtube_url = std::move(youtube_url_converted); plugin_name = "youtube"; + continue; } for(const auto &valid_plugin : valid_plugins) { @@ -1308,7 +1314,14 @@ namespace QuickMedia { pipe_body->set_items(std::move(body_items)); tabs.push_back(Tab{std::move(pipe_body), std::make_unique(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "youtube") == 0) { - if(youtube_url.empty()) { + if(!youtube_channel_url.empty()) { + auto youtube_channel_page = std::make_unique(this, youtube_channel_url, "", "Channel videos"); + tabs.push_back(Tab{create_body(false, true), std::move(youtube_channel_page), create_search_bar("Search...", 350)}); + } else if(!youtube_url.empty()) { + current_page = PageType::VIDEO_CONTENT; + auto youtube_video_page = std::make_unique(this, youtube_url, false); + video_content_page(nullptr, youtube_video_page.get(), "", false, nullptr, 0); + } else { start_tab_index = 1; tabs.push_back(Tab{create_body(false, true), std::make_unique(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{create_body(false, true), std::make_unique(this), create_search_bar("Search...", 100)}); @@ -1316,10 +1329,6 @@ namespace QuickMedia { auto history_body = create_body(false, true); auto history_page = std::make_unique(this, tabs.front().page.get(), HistoryType::YOUTUBE); tabs.push_back(Tab{std::move(history_body), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); - } else { - current_page = PageType::VIDEO_CONTENT; - auto youtube_video_page = std::make_unique(this, youtube_url, false); - video_content_page(nullptr, youtube_video_page.get(), "", false, nullptr, 0); } } else if(strcmp(plugin_name, "peertube") == 0) { if(instance.empty()) { diff --git a/src/plugins/Info.cpp b/src/plugins/Info.cpp index 252cbea..629a08f 100644 --- a/src/plugins/Info.cpp +++ b/src/plugins/Info.cpp @@ -14,6 +14,10 @@ namespace QuickMedia { return url.find("youtube.com/") != std::string::npos || url.find("youtu.be/") != std::string::npos; } + static bool is_youtube_channel_url(const std::string &url) { + return url.find("youtube.com/c/") != std::string::npos || url.find("youtu.be/c/") != std::string::npos || url.find("youtube.com/channel/") != std::string::npos || url.find("youtu.be/channel/") != std::string::npos; + } + static PluginResult open_with_browser(const std::string &url) { const char *launch_program = "xdg-open"; if(!is_program_executable_by_name("xdg-open")) { @@ -41,6 +45,9 @@ namespace QuickMedia { const std::string search_term = args.url.substr(strlen(GOOGLE_SEARCH_URL)); const std::string search_url = "https://www.google.com/search?q=" + url_param_encode(search_term); return open_with_browser(search_url); + } else if(is_youtube_channel_url(args.url)) { + result_tabs.push_back(Tab{create_body(false, true), std::make_unique(program, args.url, "", "Channel videos"), create_search_bar("Search...", 350)}); + return PluginResult::OK; } else if(is_youtube_url(args.url)) { result_tabs.push_back(Tab{nullptr, std::make_unique(program, args.url, false), nullptr}); return PluginResult::OK; @@ -66,7 +73,9 @@ namespace QuickMedia { // static std::shared_ptr InfoPage::add_url(const std::string &url) { std::string title; - if(is_youtube_url(url)) + if(is_youtube_channel_url(url)) + title = "Open youtube channel " + url; + else if(is_youtube_url(url)) title = "Play " + url; else title = "Open " + url + " in a browser"; diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 13d42bb..7318383 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -3411,7 +3411,7 @@ namespace QuickMedia { rapidjson::Document request_data(rapidjson::kObjectType); request_data.AddMember("msgtype", rapidjson::StringRef(file_info ? content_type_to_message_type(file_info->content_type) : "m.text"), request_data.GetAllocator()); - request_data.AddMember("body", rapidjson::StringRef(message_reply_body.c_str()), request_data.GetAllocator()); + request_data.AddMember("body", rapidjson::StringRef(file_info ? body.c_str() : message_reply_body.c_str()), request_data.GetAllocator()); request_data.AddMember("format", "org.matrix.custom.html", request_data.GetAllocator()); request_data.AddMember("formatted_body", rapidjson::StringRef(formatted_message_reply_body.c_str()), request_data.GetAllocator()); request_data.AddMember("m.relates_to", std::move(relates_to_json), request_data.GetAllocator()); diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 4dbb6c4..f2fb36e 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -91,6 +91,54 @@ namespace QuickMedia { return false; } + bool youtube_url_extract_channel_id(const std::string &youtube_url, std::string &channel_id, std::string &channel_url) { + size_t index = youtube_url.find("youtube.com/c/"); + if(index != std::string::npos) { + index += 14; + size_t end_index = youtube_url.find("/", index); + if(end_index == std::string::npos) + end_index = youtube_url.size(); + channel_id = youtube_url.substr(index, end_index - index); + channel_url = "https://www.youtube.com/c/" + channel_id; + return true; + } + + index = youtube_url.find("youtu.be/c/"); + if(index != std::string::npos) { + index += 11; + size_t end_index = youtube_url.find("/", index); + if(end_index == std::string::npos) + end_index = youtube_url.size(); + channel_id = youtube_url.substr(index, end_index - index); + channel_url = "https://www.youtube.com/c/" + channel_id; + return true; + } + + index = youtube_url.find("youtube.com/channel/"); + if(index != std::string::npos) { + index += 20; + size_t end_index = youtube_url.find("/", index); + if(end_index == std::string::npos) + end_index = youtube_url.size(); + channel_id = youtube_url.substr(index, end_index - index); + channel_url = "https://www.youtube.com/channel/" + channel_id; + return true; + } + + index = youtube_url.find("youtu.be/channel/"); + if(index != std::string::npos) { + index += 17; + size_t end_index = youtube_url.find("/", index); + if(end_index == std::string::npos) + end_index = youtube_url.size(); + channel_id = youtube_url.substr(index, end_index - index); + channel_url = "https://www.youtube.com/channel/" + channel_id; + return true; + } + + return false; + } + static std::mutex cookies_mutex; static std::string cookies_filepath; static std::string api_key; @@ -687,10 +735,20 @@ namespace QuickMedia { return ""; } - static void parse_channel_videos(const Json::Value &json_root, std::string &continuation_token, std::unordered_set &added_videos, BodyItems &body_items) { + static void parse_channel_videos(const Json::Value &json_root, std::string &continuation_token, std::unordered_set &added_videos, std::string &browse_id, BodyItems &body_items) { if(!json_root.isObject()) return; + const Json::Value &endpoint_json = json_root["endpoint"]; + if(endpoint_json.isObject()) { + const Json::Value &browse_endpoint_json = endpoint_json["browseEndpoint"]; + if(browse_endpoint_json.isObject()) { + const Json::Value &browse_id_json = browse_endpoint_json["browseId"]; + if(browse_id_json.isString()) + browse_id = browse_id_json.asString(); + } + } + const Json::Value *response_json = &json_root["response"]; if(!response_json->isObject()) response_json = &json_root; @@ -1415,19 +1473,6 @@ namespace QuickMedia { return fetch_comments(this, video_url, continuation_token, result_items); } - static std::string channel_url_extract_id(const std::string &channel_url) { - size_t index = channel_url.find("channel/"); - if(index == std::string::npos) - return ""; - - index += 8; - size_t end_index = channel_url.find('/', index); - if(end_index == std::string::npos) - return channel_url.substr(index); - - return channel_url.substr(index, end_index - index); - } - SearchResult YoutubeChannelPage::search(const std::string &str, BodyItems &result_items) { added_videos.clear(); continuation_token.clear(); @@ -1435,6 +1480,13 @@ namespace QuickMedia { if(str.empty()) return plugin_result_to_search_result(lazy_fetch(result_items)); + std::string channel_id; + std::string channel_url; + if(!youtube_url_extract_channel_id(url, channel_id, channel_url)) { + fprintf(stderr, "Error: failed to extract youtube channel id from url: %s\n", url.c_str()); + return SearchResult::ERR; + } + std::vector cookies = get_cookies(); std::string next_url = "https://www.youtube.com/youtubei/v1/browse?key=" + url_param_encode(api_key) + "&gl=US&hl=en&prettyPrint=false"; @@ -1453,7 +1505,7 @@ namespace QuickMedia { client_json["originalUrl"] = url + "/videos"; context_json["client"] = std::move(client_json); request_json["context"] = std::move(context_json); - request_json["browseId"] = channel_url_extract_id(url); + request_json["browseId"] = channel_id; request_json["query"] = str; request_json["params"] = "EgZzZWFyY2g%3D"; //request_json["continuation"] = current_continuation_token; @@ -1668,35 +1720,29 @@ namespace QuickMedia { DownloadResult result = download_json(json_root, url + "/videos?pbj=1&gl=US&hl=en", std::move(additional_args), true); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); + std::string browse_id; if(json_root.isObject()) { - search_page_submit_suggestion_handler(json_root, continuation_token, result_items, added_videos); - parse_channel_videos(json_root, continuation_token, added_videos, result_items); - return PluginResult::OK; - } - - if(!json_root.isArray()) + //search_page_submit_suggestion_handler(json_root, continuation_token, result_items, added_videos); + parse_channel_videos(json_root, continuation_token, added_videos, browse_id, result_items); + } else if(json_root.isArray()) { + for(const Json::Value &json_item : json_root) { + //search_page_submit_suggestion_handler(json_root, continuation_token, result_items, added_videos); + parse_channel_videos(json_item, continuation_token, added_videos, browse_id, result_items); + } + } else { return PluginResult::ERR; - - for(const Json::Value &json_item : json_root) { - search_page_submit_suggestion_handler(json_root, continuation_token, result_items, added_videos); - parse_channel_videos(json_item, continuation_token, added_videos, result_items); } + + if(!browse_id.empty()) + url = "https://www.youtube.com/channel/" + std::move(browse_id); return PluginResult::OK; } TrackResult YoutubeChannelPage::track(const std::string&) { - size_t channel_id_start = url.find("/channel/"); - if(channel_id_start == std::string::npos) { - show_notification("QuickMedia", "Unable to get channel id from " + url, Urgency::CRITICAL); - return TrackResult::ERR; - } - - channel_id_start += 9; - size_t channel_id_end = url.find('/', channel_id_start); - if(channel_id_end == std::string::npos) channel_id_end = url.size(); - std::string channel_id = url.substr(channel_id_start, channel_id_end - channel_id_start); - if(channel_id.empty()) { + std::string channel_id; + std::string channel_url; + if(!youtube_url_extract_channel_id(url, channel_id, channel_url)) { show_notification("QuickMedia", "Unable to get channel id from " + url, Urgency::CRITICAL); return TrackResult::ERR; } -- cgit v1.2.3