aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO1
-rw-r--r--plugins/Page.hpp2
-rw-r--r--plugins/Plugin.hpp1
-rw-r--r--plugins/Youtube.hpp2
-rw-r--r--src/QuickMedia.cpp94
-rw-r--r--src/plugins/Plugin.cpp1
-rw-r--r--src/plugins/Youtube.cpp253
7 files changed, 226 insertions, 128 deletions
diff --git a/TODO b/TODO
index f8183e4..cbffebb 100644
--- a/TODO
+++ b/TODO
@@ -154,7 +154,6 @@ Update thumbnails in file-manager if an image is replaced, by including the modi
Create a workaround for dwm terminal swallow patch stealing mpv when moving QuickMedia to another monitor sometimes. Maybe check for structure notify events on mpv and reparent and select input on the mpv window again?
Resize video thumbnail when extracted with ffmpeg to 480x360, clamped from its original size.
Add option to decline and mute user in invites. This is to combat invite spam, where muted users cant invite you.
-Searching in channel page should search in the channel instead of filter, because with filtering we only filter the videos we have loaded in the channel page and the channel page uses pagination; so we may have only loaded 20 videos while the channel may actually have 2000 videos.
Allow hiding videos so they dont show up in recommendations and related videos.
Add an option to select video resolution, if we want to use less power and less bandwidth for example.
Use mpv option --gpu-context=x11egl on pinephone to force xwayland on wayland, to be able to embed the mpv window inside the quickmedia.
diff --git a/plugins/Page.hpp b/plugins/Page.hpp
index 54e7383..e720f14 100644
--- a/plugins/Page.hpp
+++ b/plugins/Page.hpp
@@ -84,7 +84,7 @@ namespace QuickMedia {
class LazyFetchPage : public Page {
public:
LazyFetchPage(Program *program) : Page(program) {}
- bool search_is_filter() override { return true; }
+ virtual bool search_is_filter() override { return true; }
bool is_lazy_fetch_page() const override { return true; }
virtual PluginResult lazy_fetch(BodyItems &result_items) = 0;
};
diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp
index 2026319..ebf3408 100644
--- a/plugins/Plugin.hpp
+++ b/plugins/Plugin.hpp
@@ -40,4 +40,5 @@ namespace QuickMedia {
SearchResult download_result_to_search_result(DownloadResult download_result);
ImageResult download_result_to_image_result(DownloadResult download_result);
PluginResult search_result_to_plugin_result(SearchResult search_result);
+ SearchResult plugin_result_to_search_result(PluginResult plugin_result);
} \ No newline at end of file
diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp
index 1a66e5c..c0bb429 100644
--- a/plugins/Youtube.hpp
+++ b/plugins/Youtube.hpp
@@ -25,6 +25,8 @@ namespace QuickMedia {
public:
YoutubeChannelPage(Program *program, std::string url, std::string continuation_token, std::string title) : LazyFetchPage(program), url(std::move(url)), continuation_token(std::move(continuation_token)), title(std::move(title)) {}
const char* get_title() const override { return title.c_str(); }
+ bool search_is_filter() override { return false; }
+ 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;
PluginResult lazy_fetch(BodyItems &result_items) override;
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 6b480d2..03fe6b2 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -72,62 +72,53 @@ static const XRRModeInfo* get_mode_info(const XRRScreenResources *sr, RRMode id)
return nullptr;
}
-static int get_monitor_max_hz(Display *display) {
+static void for_each_active_monitor_output(Display *display, std::function<void(const XRRModeInfo*)> callback_func) {
XRRScreenResources *screen_res = XRRGetScreenResources(display, DefaultRootWindow(display));
- if(screen_res) {
- unsigned long max_hz = 0;
- for(int i = 0; i < screen_res->noutput; ++i) {
- XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]);
- if(out_info && out_info->connection == RR_Connected) {
- XRRCrtcInfo *crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc);
- if(crt_info) {
- const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode);
- if(mode_info) {
- unsigned long total = mode_info->hTotal * mode_info->vTotal;
- if(total > 0)
- max_hz = std::max(max_hz, mode_info->dotClock / total);
- }
- XRRFreeCrtcInfo(crt_info);
- }
+ if(!screen_res)
+ return;
+
+ for(int i = 0; i < screen_res->noutput; ++i) {
+ XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]);
+ if(out_info && out_info->connection == RR_Connected) {
+ XRRCrtcInfo *crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc);
+ if(crt_info) {
+ const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode);
+ if(mode_info)
+ callback_func(mode_info);
+ XRRFreeCrtcInfo(crt_info);
}
- if(out_info)
- XRRFreeOutputInfo(out_info);
}
- XRRFreeScreenResources(screen_res);
- if(max_hz == 0)
- max_hz = 60;
- return std::min(max_hz, 144UL);
+ if(out_info)
+ XRRFreeOutputInfo(out_info);
}
- return 60;
+
+ XRRFreeScreenResources(screen_res);
+}
+
+static int get_monitor_max_hz(Display *display) {
+ unsigned long max_hz = 0;
+ for_each_active_monitor_output(display, [&max_hz](const XRRModeInfo *mode_info) {
+ unsigned long total = mode_info->hTotal * mode_info->vTotal;
+ if(total > 0)
+ max_hz = std::max(max_hz, (unsigned long)std::round((double)mode_info->dotClock / (double)total));
+ });
+
+ if(max_hz == 0)
+ return 60;
+ return std::min(max_hz, 144UL);
}
static int get_largest_monitor_height(Display *display) {
- XRRScreenResources *screen_res = XRRGetScreenResources(display, DefaultRootWindow(display));
- if(screen_res) {
- int max_height = 0;
- for(int i = 0; i < screen_res->noutput; ++i) {
- XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]);
- if(out_info && out_info->connection == RR_Connected) {
- XRRCrtcInfo* crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc);
- if(crt_info) {
- const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode);
- if(mode_info) {
- // Need to get the min of width or height because we want to get the smallest size for monitors in portrait mode, for mobile devices such as pinephone
- int width_or_height = std::min((int)mode_info->width, (int)mode_info->height);
- max_height = std::max(max_height, width_or_height);
- }
- XRRFreeCrtcInfo(crt_info);
- }
- }
- if(out_info)
- XRRFreeOutputInfo(out_info);
- }
- XRRFreeScreenResources(screen_res);
- if(max_height == 0)
- max_height = 720;
- return std::max(max_height, 480);
- }
- return 720;
+ int max_height = 0;
+ for_each_active_monitor_output(display, [&max_height](const XRRModeInfo *mode_info) {
+ // Need to get the min of width or height because we want to get the smallest size for monitors in portrait mode, for mobile devices such as pinephone
+ int width_or_height = std::min((int)mode_info->width, (int)mode_info->height);
+ max_height = std::max(max_height, width_or_height);
+ });
+
+ if(max_height == 0)
+ return 720;
+ return std::max(max_height, 480);
}
static void get_screen_resolution(Display *display, int *width, int *height) {
@@ -1903,7 +1894,7 @@ namespace QuickMedia {
tabs.push_back(Tab{std::move(related_videos_body), std::move(related_videos_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
}
if(channels_page) {
- tabs.push_back(Tab{create_body(), std::move(channels_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{create_body(), std::move(channels_page), create_search_bar("Search...", is_youtube ? 350 : SEARCH_DELAY_FILTER)});
}
bool page_changed = false;
@@ -2518,6 +2509,9 @@ namespace QuickMedia {
images_to_upscale_queue.clear();
image_upscale_status.clear();
}
+
+ window_size.x = window.getSize().x;
+ window_size.y = window.getSize().y;
}
static bool is_url_video(const std::string &url) {
diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp
index a391d06..33fe47e 100644
--- a/src/plugins/Plugin.cpp
+++ b/src/plugins/Plugin.cpp
@@ -6,4 +6,5 @@ namespace QuickMedia {
SearchResult download_result_to_search_result(DownloadResult download_result) { return (SearchResult)download_result; }
ImageResult download_result_to_image_result(DownloadResult download_result) { return (ImageResult)download_result; }
PluginResult search_result_to_plugin_result(SearchResult search_result) { return (PluginResult)search_result; }
+ SearchResult plugin_result_to_search_result(PluginResult plugin_result) { return (SearchResult)plugin_result; }
} \ No newline at end of file
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index cc20f2f..b3c9a60 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -3,6 +3,7 @@
#include "../../include/NetUtils.hpp"
#include "../../include/StringUtils.hpp"
#include "../../include/Scale.hpp"
+#include <json/writer.h>
#include <string.h>
namespace QuickMedia {
@@ -250,28 +251,10 @@ namespace QuickMedia {
return token_json.asString();
}
- static std::string grid_renderer_get_continuation_token(const Json::Value &grid_renderer_json) {
- const Json::Value &continuations_json = grid_renderer_json["continuations"];
- if(!continuations_json.isArray())
- return "";
-
- for(const Json::Value &continuation_json : continuations_json) {
- if(!continuation_json.isObject())
- continue;
-
- const Json::Value &next_continuation_data_json = continuation_json["nextContinuationData"];
- if(!next_continuation_data_json.isObject())
- continue;
-
- const Json::Value &continuation_item_json = next_continuation_data_json["continuation"];
- if(continuation_item_json.isString())
- return continuation_item_json.asString();
- }
-
- return "";
- }
-
static void parse_item_section_renderer(const Json::Value &item_section_renderer_json, std::unordered_set<std::string> &added_videos, BodyItems &result_items) {
+ if(!item_section_renderer_json.isObject())
+ return;
+
const Json::Value &item_contents_json = item_section_renderer_json["contents"];
if(!item_contents_json.isArray())
return;
@@ -399,9 +382,6 @@ namespace QuickMedia {
if(!grid_renderer_json.isObject())
continue;
- if(new_continuation_token.empty())
- new_continuation_token = grid_renderer_get_continuation_token(grid_renderer_json);
-
const Json::Value &items_json = grid_renderer_json["items"];
if(!items_json.isArray())
continue;
@@ -410,6 +390,9 @@ namespace QuickMedia {
if(!item_json.isObject())
continue;
+ if(new_continuation_token.empty())
+ new_continuation_token = item_section_renderer_get_continuation_token(item_json);
+
const Json::Value &grid_video_renderer = item_json["gridVideoRenderer"];
if(!grid_video_renderer.isObject())
continue;
@@ -429,6 +412,29 @@ namespace QuickMedia {
return body_items;
}
+ static void parse_section_list_renderer(const Json::Value &section_list_renderer_json, std::string &continuation_token, BodyItems &result_items, std::unordered_set<std::string> &added_videos) {
+ if(!section_list_renderer_json.isObject())
+ return;
+
+ const Json::Value &contents2_json = section_list_renderer_json["contents"];
+ if(!contents2_json.isArray())
+ return;
+
+ for(const Json::Value &item_json : contents2_json) {
+ if(!item_json.isObject())
+ continue;
+
+ if(continuation_token.empty())
+ continuation_token = item_section_renderer_get_continuation_token(item_json);
+
+ const Json::Value &item_section_renderer_json = item_json["itemSectionRenderer"];
+ if(!item_section_renderer_json.isObject())
+ continue;
+
+ parse_item_section_renderer(item_section_renderer_json, added_videos, result_items);
+ }
+ }
+
SearchResult YoutubeSearchPage::search(const std::string &str, BodyItems &result_items) {
continuation_token.clear();
current_page = 0;
@@ -474,27 +480,7 @@ namespace QuickMedia {
if(!primary_contents_json.isObject())
continue;
- const Json::Value &section_list_renderer_json = primary_contents_json["sectionListRenderer"];
- if(!section_list_renderer_json.isObject())
- continue;
-
- const Json::Value &contents2_json = section_list_renderer_json["contents"];
- if(!contents2_json.isArray())
- continue;
-
- for(const Json::Value &item_json : contents2_json) {
- if(!item_json.isObject())
- continue;
-
- if(continuation_token.empty())
- continuation_token = item_section_renderer_get_continuation_token(item_json);
-
- const Json::Value &item_section_renderer_json = item_json["itemSectionRenderer"];
- if(!item_section_renderer_json.isObject())
- continue;
-
- parse_item_section_renderer(item_section_renderer_json, added_videos, result_items);
- }
+ parse_section_list_renderer(primary_contents_json["sectionListRenderer"], continuation_token, result_items, added_videos);
}
return SearchResult::OK;
@@ -515,7 +501,7 @@ namespace QuickMedia {
if(strncmp(url.c_str(), "https://www.youtube.com/channel/", 32) == 0) {
// TODO: Make all pages (for all services) lazy fetch in a similar manner!
- result_tabs.push_back(Tab{create_body(), std::make_unique<YoutubeChannelPage>(program, url, "", title), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ result_tabs.push_back(Tab{create_body(), std::make_unique<YoutubeChannelPage>(program, url, "", title), create_search_bar("Search...", 350)});
} else {
result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program), nullptr});
}
@@ -592,6 +578,102 @@ namespace QuickMedia {
return PluginResult::OK;
}
+ 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();
+ current_page = 0;
+ if(str.empty())
+ return plugin_result_to_search_result(lazy_fetch(result_items));
+
+ std::string next_url = "https://www.youtube.com/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
+
+ Json::Value request_json(Json::objectValue);
+ Json::Value context_json(Json::objectValue);
+ Json::Value client_json(Json::objectValue);
+ client_json["hl"] = "en";
+ client_json["gl"] = "US";
+ client_json["deviceMake"] = "";
+ client_json["deviceModel"] = "";
+ client_json["userAgent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
+ client_json["clientName"] = "WEB";
+ client_json["clientVersion"] = "2.20210304.08.01";
+ client_json["osName"] = "X11";
+ client_json["osVersion"] = "";
+ 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["query"] = str;
+ request_json["params"] = "EgZzZWFyY2g%3D";
+ //request_json["continuation"] = current_continuation_token;
+
+ Json::StreamWriterBuilder json_builder;
+ json_builder["commentStyle"] = "None";
+ json_builder["indentation"] = "";
+
+ std::vector<CommandArg> additional_args = {
+ { "-H", "authority: www.youtube.com" },
+ { "-H", "x-origin: https://www.youtube.com" },
+ { "-H", "content-type: application/json" },
+ { "-H", "x-youtube-client-name: 1" },
+ { "-H", "x-youtube-client-version: 2.20200626.03.00" },
+ { "-H", "referer: " + url + "/videos" },
+ { "--data-raw", Json::writeString(json_builder, request_json) }
+ };
+
+ //std::vector<CommandArg> cookies = get_cookies();
+ //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end());
+
+ Json::Value json_root;
+ DownloadResult result = download_json(json_root, next_url, std::move(additional_args), true);
+ if(result != DownloadResult::OK) return download_result_to_search_result(result);
+
+ if(!json_root.isObject())
+ return SearchResult::ERR;
+
+ const Json::Value &contents_json = json_root["contents"];
+ if(!contents_json.isObject())
+ return SearchResult::ERR;
+
+ const Json::Value &two_column_browse_results_renderer_json = contents_json["twoColumnBrowseResultsRenderer"];
+ if(!two_column_browse_results_renderer_json.isObject())
+ return SearchResult::ERR;
+
+ const Json::Value &tabs_json = two_column_browse_results_renderer_json["tabs"];
+ if(!tabs_json.isArray())
+ return SearchResult::ERR;
+
+ for(const Json::Value &json_item : tabs_json) {
+ if(!json_item.isObject())
+ continue;
+
+ const Json::Value &expandable_tab_renderer_json = json_item["expandableTabRenderer"];
+ if(!expandable_tab_renderer_json.isObject())
+ continue;
+
+ const Json::Value &content_json = expandable_tab_renderer_json["content"];
+ if(!content_json.isObject())
+ continue;
+
+ parse_section_list_renderer(content_json["sectionListRenderer"], continuation_token, result_items, added_videos);
+ }
+
+ return SearchResult::OK;
+ }
+
PluginResult YoutubeChannelPage::get_page(const std::string&, int page, BodyItems &result_items) {
while(current_page < page) {
PluginResult plugin_result = search_get_continuation(url, continuation_token, result_items);
@@ -602,14 +684,37 @@ namespace QuickMedia {
}
PluginResult YoutubeChannelPage::search_get_continuation(const std::string &url, const std::string &current_continuation_token, BodyItems &result_items) {
- std::string next_url = "https://www.youtube.com/browse_ajax?ctoken=" + current_continuation_token + "&continuation=" + current_continuation_token;
+ std::string next_url = "https://www.youtube.com/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
+
+ Json::Value request_json(Json::objectValue);
+ Json::Value context_json(Json::objectValue);
+ Json::Value client_json(Json::objectValue);
+ client_json["hl"] = "en";
+ client_json["gl"] = "US";
+ client_json["deviceMake"] = "";
+ client_json["deviceModel"] = "";
+ client_json["userAgent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
+ client_json["clientName"] = "WEB";
+ client_json["clientVersion"] = "2.20210304.08.01";
+ client_json["osName"] = "X11";
+ client_json["osVersion"] = "";
+ client_json["originalUrl"] = url + "/videos";
+ context_json["client"] = std::move(client_json);
+ request_json["context"] = std::move(context_json);
+ request_json["continuation"] = current_continuation_token;
+
+ Json::StreamWriterBuilder json_builder;
+ json_builder["commentStyle"] = "None";
+ json_builder["indentation"] = "";
std::vector<CommandArg> additional_args = {
- { "-H", "x-spf-referer: " + url },
+ { "-H", "authority: www.youtube.com" },
+ { "-H", "x-origin: https://www.youtube.com" },
+ { "-H", "content-type: application/json" },
{ "-H", "x-youtube-client-name: 1" },
- { "-H", "x-spf-previous: " + url },
{ "-H", "x-youtube-client-version: 2.20200626.03.00" },
- { "-H", "referer: " + url }
+ { "-H", "referer: " + url + "/videos" },
+ { "--data-raw", Json::writeString(json_builder, request_json) }
};
//std::vector<CommandArg> cookies = get_cookies();
@@ -619,46 +724,41 @@ namespace QuickMedia {
DownloadResult result = download_json(json_root, next_url, std::move(additional_args), true);
if(result != DownloadResult::OK) return download_result_to_plugin_result(result);
- if(!json_root.isArray())
+ if(!json_root.isObject())
+ return PluginResult::ERR;
+
+ const Json::Value &on_response_received_actions_json = json_root["onResponseReceivedActions"];
+ if(!on_response_received_actions_json.isArray())
return PluginResult::ERR;
std::string new_continuation_token;
- for(const Json::Value &json_item : json_root) {
+ for(const Json::Value &json_item : on_response_received_actions_json) {
if(!json_item.isObject())
continue;
- const Json::Value &response_json = json_item["response"];
- if(!response_json.isObject())
- continue;
-
- const Json::Value &continuation_contents_json = response_json["continuationContents"];
- if(!continuation_contents_json.isObject())
- continue;
-
- const Json::Value &grid_continuation_json = continuation_contents_json["gridContinuation"];
- if(!grid_continuation_json.isObject())
+ const Json::Value &append_continuation_items_action_json = json_item["appendContinuationItemsAction"];
+ if(!append_continuation_items_action_json.isObject())
continue;
- if(new_continuation_token.empty()) {
- // grid_continuation_json is compatible with grid_renderer
- new_continuation_token = grid_renderer_get_continuation_token(grid_continuation_json);
- }
-
- const Json::Value &items_json = grid_continuation_json["items"];
- if(!items_json.isArray())
+ const Json::Value &continuation_items_json = append_continuation_items_action_json["continuationItems"];
+ if(!continuation_items_json.isArray())
continue;
- for(const Json::Value &item_json : items_json) {
+ for(const Json::Value &item_json : continuation_items_json) {
if(!item_json.isObject())
continue;
- const Json::Value &grid_video_renderer = item_json["gridVideoRenderer"];
- if(!grid_video_renderer.isObject())
- continue;
+ if(new_continuation_token.empty())
+ new_continuation_token = item_section_renderer_get_continuation_token(item_json);
- auto body_item = parse_common_video_item(grid_video_renderer, added_videos);
- if(body_item)
- result_items.push_back(std::move(body_item));
+ const Json::Value &grid_video_renderer = item_json["gridVideoRenderer"];
+ if(grid_video_renderer.isObject()) {
+ auto body_item = parse_common_video_item(grid_video_renderer, added_videos);
+ if(body_item)
+ result_items.push_back(std::move(body_item));
+ } else {
+ parse_item_section_renderer(item_json["itemSectionRenderer"], added_videos, result_items);
+ }
}
}
@@ -676,6 +776,7 @@ namespace QuickMedia {
}
PluginResult YoutubeChannelPage::lazy_fetch(BodyItems &result_items) {
+ added_videos.clear();
std::vector<CommandArg> additional_args = {
{ "-H", "x-spf-referer: " + url },
{ "-H", "x-youtube-client-name: 1" },