aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--TODO5
-rw-r--r--include/QuickMedia.hpp1
-rw-r--r--plugins/Youtube.hpp3
-rw-r--r--src/QuickMedia.cpp25
-rw-r--r--src/plugins/Info.cpp11
-rw-r--r--src/plugins/Matrix.cpp2
-rw-r--r--src/plugins/Youtube.cpp118
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 <directory>] [-e <window>] [youtube-url]
+usage: quickmedia [plugin] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <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<VideoPlayer> 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<bool()> 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 <directory>] [-e <window>] [youtube-url]\n");
+ fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <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<PipePage>(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<YoutubeChannelPage>(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<YoutubeVideoPage>(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<YoutubeSubscriptionsPage>(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
tabs.push_back(Tab{create_body(false, true), std::make_unique<YoutubeSearchPage>(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<HistoryPage>(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<YoutubeVideoPage>(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<YoutubeChannelPage>(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<YoutubeVideoPage>(program, args.url, false), nullptr});
return PluginResult::OK;
@@ -66,7 +73,9 @@ namespace QuickMedia {
// static
std::shared_ptr<BodyItem> 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<std::string> &added_videos, BodyItems &body_items) {
+ static void parse_channel_videos(const Json::Value &json_root, std::string &continuation_token, std::unordered_set<std::string> &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<CommandArg> 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;
}