aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Body.cpp1
-rw-r--r--src/QuickMedia.cpp42
-rw-r--r--src/plugins/Pornhub.cpp8
-rw-r--r--src/plugins/Soundcloud.cpp287
-rw-r--r--src/plugins/Spotify.cpp4
-rw-r--r--src/plugins/Youtube.cpp10
6 files changed, 328 insertions, 24 deletions
diff --git a/src/Body.cpp b/src/Body.cpp
index de924da..e4d4e97 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -98,6 +98,7 @@ namespace QuickMedia {
title_color = other.title_color;
author_color = other.author_color;
description_color = other.description_color;
+ extra = other.extra;
return *this;
}
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index cb570fc..ff78735 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -9,6 +9,7 @@
#include "../plugins/Matrix.hpp"
#include "../plugins/Pleroma.hpp"
#include "../plugins/Spotify.hpp"
+#include "../plugins/Soundcloud.hpp"
#include "../plugins/FileManager.hpp"
#include "../plugins/Pipe.hpp"
#include "../include/Scale.hpp"
@@ -453,7 +454,7 @@ namespace QuickMedia {
static void usage() {
fprintf(stderr, "usage: QuickMedia <plugin> [--tor] [--no-video] [--use-system-mpv-config] [--dir <directory>]\n");
fprintf(stderr, "OPTIONS:\n");
- fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube, spotify, nyaa.si, matrix, file-manager or pipe\n");
+ fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube, spotify, soundcloud, nyaa.si, matrix, file-manager or pipe\n");
fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n");
fprintf(stderr, " --tor Use tor. Disabled by default\n");
fprintf(stderr, " --use-system-mpv-config Use system mpv config instead of no config. Disabled by default\n");
@@ -497,6 +498,10 @@ namespace QuickMedia {
plugin_name = argv[i];
plugin_logo_path = resources_root + "images/spotify_logo.png";
no_video = true;
+ } else if(strcmp(argv[i], "soundcloud") == 0) {
+ plugin_name = argv[i];
+ plugin_logo_path = resources_root + "images/soundcloud_logo.png";
+ no_video = true;
} else if(strcmp(argv[i], "pornhub") == 0) {
plugin_name = argv[i];
plugin_logo_path = resources_root + "images/pornhub_logo.png";
@@ -689,6 +694,9 @@ namespace QuickMedia {
} else if(strcmp(plugin_name, "spotify") == 0) {
auto search_body = create_body();
tabs.push_back(Tab{std::move(search_body), std::make_unique<SpotifyPodcastSearchPage>(this), create_search_bar("Search...", 250)});
+ } else if(strcmp(plugin_name, "soundcloud") == 0) {
+ auto search_body = create_body();
+ tabs.push_back(Tab{std::move(search_body), std::make_unique<SoundcloudSearchPage>(this), create_search_bar("Search...", 500)});
} else if(strcmp(plugin_name, "mastodon") == 0 || strcmp(plugin_name, "pleroma") == 0) {
auto pleroma = std::make_shared<Pleroma>();
auto search_body = create_body();
@@ -1106,11 +1114,12 @@ namespace QuickMedia {
std::function<void()> submit_handler;
submit_handler = [this, &submit_handler, &after_submit_handler, &json_chapters, &tabs, &tab_associated_data, &selected_tab, &loop_running, &redraw]() {
- BodyItem *selected_item = tabs[selected_tab].body->get_selected();
+ auto selected_item = tabs[selected_tab].body->get_selected_shared();
if(!selected_item)
return;
std::vector<Tab> new_tabs;
+ tabs[selected_tab].page->submit_body_item = selected_item;
PluginResult submit_result = tabs[selected_tab].page->submit(selected_item->get_title(), selected_item->url, new_tabs);
if(submit_result != PluginResult::OK) {
// TODO: Show the exact cause of error (get error message from curl).
@@ -1135,11 +1144,14 @@ namespace QuickMedia {
tabs[selected_tab].body = std::move(new_tabs[0].body);
else
loop_running = false;
+ tabs[selected_tab].page->submit_body_item = nullptr;
return;
}
- if(new_tabs.empty())
+ if(new_tabs.empty()) {
+ tabs[selected_tab].page->submit_body_item = nullptr;
return;
+ }
if(after_submit_handler)
after_submit_handler(new_tabs);
@@ -1151,7 +1163,7 @@ namespace QuickMedia {
hide_virtual_keyboard();
if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::MANGA_IMAGES) {
- select_episode(selected_item, false);
+ select_episode(selected_item.get(), false);
Body *chapters_body = tabs[selected_tab].body.get();
chapters_body->filter_search_fuzzy(""); // Needed (or not really) to go to the next chapter when reaching the last page of a chapter
MangaImagesPage *manga_images_page = static_cast<MangaImagesPage*>(new_tabs[0].page.get());
@@ -1188,9 +1200,9 @@ namespace QuickMedia {
image_board_thread_page(static_cast<ImageBoardThreadPage*>(new_tabs[0].page.get()), new_tabs[0].body.get());
} else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::VIDEO) {
current_page = PageType::VIDEO_CONTENT;
- video_content_page(static_cast<VideoPage*>(new_tabs[0].page.get()), selected_item->url, selected_item->get_title(), false);
+ video_content_page(static_cast<VideoPage*>(new_tabs[0].page.get()), selected_item->get_title(), false);
} else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::CHAT) {
- body_set_selected_item(tabs[selected_tab].body.get(), selected_item);
+ body_set_selected_item(tabs[selected_tab].body.get(), selected_item.get());
current_page = PageType::CHAT;
current_chat_room = matrix->get_room_by_id(selected_item->url);
@@ -1225,6 +1237,7 @@ namespace QuickMedia {
json_chapters = &chapters_json;
}
+ tabs[selected_tab].page->submit_body_item = nullptr;
redraw = true;
hide_virtual_keyboard();
};
@@ -1727,7 +1740,7 @@ namespace QuickMedia {
#define CLEANMASK(mask) ((mask) & (ShiftMask|ControlMask|Mod1Mask|Mod4Mask|Mod5Mask))
- void Program::video_content_page(VideoPage *video_page, std::string video_url, std::string video_title, bool download_if_streaming_fails) {
+ void Program::video_content_page(VideoPage *video_page, std::string video_title, bool download_if_streaming_fails) {
sf::Clock time_watched_timer;
bool added_recommendations = false;
bool video_loaded = false;
@@ -1735,6 +1748,7 @@ namespace QuickMedia {
const bool is_matrix = strcmp(plugin_name, "matrix") == 0;
PageType previous_page = pop_page_stack();
+ std::string video_url = video_page->get_url();
bool video_url_is_local = false;
if(download_if_streaming_fails) {
@@ -2824,7 +2838,8 @@ namespace QuickMedia {
current_page = PageType::VIDEO_CONTENT;
watched_videos.clear();
// TODO: Use real title
- video_content_page(thread_page, selected_item->attached_content_url, "No title.webm", true);
+ thread_page->video_url = selected_item->attached_content_url;
+ video_content_page(thread_page, "No title.webm", true);
redraw = true;
} else {
if(downloading_image && load_image_future.valid())
@@ -4117,7 +4132,8 @@ namespace QuickMedia {
watched_videos.clear();
current_page = PageType::VIDEO_CONTENT;
// TODO: Add title
- video_content_page(video_page.get(), url, "No title", false);
+ video_page->url = url;
+ video_content_page(video_page.get(), "No title", false);
redraw = true;
} else {
const char *launch_program = "xdg-open";
@@ -4186,8 +4202,7 @@ namespace QuickMedia {
if(selected_item_message) {
MessageType message_type = selected_item_message->type;
- std::string *selected_url = &selected->url;
- if(!selected_url->empty()) {
+ if(!selected->url.empty()) {
if(message_type == MessageType::VIDEO || message_type == MessageType::IMAGE || message_type == MessageType::AUDIO) {
page_stack.push(PageType::CHAT);
watched_videos.clear();
@@ -4196,13 +4211,14 @@ namespace QuickMedia {
bool prev_no_video = no_video;
no_video = is_audio;
// TODO: Add title
- video_content_page(video_page.get(), *selected_url, "No title", message_type == MessageType::VIDEO || message_type == MessageType::AUDIO);
+ video_page->url = selected->url;
+ video_content_page(video_page.get(), "No title", message_type == MessageType::VIDEO || message_type == MessageType::AUDIO);
no_video = prev_no_video;
redraw = true;
return true;
}
- launch_url(*selected_url);
+ launch_url(selected->url);
return true;
}
}
diff --git a/src/plugins/Pornhub.cpp b/src/plugins/Pornhub.cpp
index b4e908a..74b66b6 100644
--- a/src/plugins/Pornhub.cpp
+++ b/src/plugins/Pornhub.cpp
@@ -140,13 +140,13 @@ namespace QuickMedia {
return search_result_to_plugin_result(get_videos_in_page(url, is_tor_enabled(), result_items));
}
- PluginResult PornhubSearchPage::submit(const std::string&, const std::string&, std::vector<Tab> &result_tabs) {
- result_tabs.push_back(Tab{nullptr, std::make_unique<PornhubVideoPage>(program), nullptr});
+ PluginResult PornhubSearchPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{nullptr, std::make_unique<PornhubVideoPage>(program, url), nullptr});
return PluginResult::OK;
}
- PluginResult PornhubRelatedVideosPage::submit(const std::string&, const std::string&, std::vector<Tab> &result_tabs) {
- result_tabs.push_back(Tab{nullptr, std::make_unique<PornhubVideoPage>(program), nullptr});
+ PluginResult PornhubRelatedVideosPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{nullptr, std::make_unique<PornhubVideoPage>(program, url), nullptr});
return PluginResult::OK;
}
diff --git a/src/plugins/Soundcloud.cpp b/src/plugins/Soundcloud.cpp
new file mode 100644
index 0000000..cfb6011
--- /dev/null
+++ b/src/plugins/Soundcloud.cpp
@@ -0,0 +1,287 @@
+#include "../../plugins/Soundcloud.hpp"
+#include "../../include/NetUtils.hpp"
+#include "../../include/StringUtils.hpp"
+#include "../../include/Scale.hpp"
+
+namespace QuickMedia {
+ static std::string client_id = "Na04L87fnpWDMVCCW2ngWldN4JMoLTAc";
+
+ class SoundcloudPlaylist : public BodyItemExtra {
+ public:
+ BodyItems tracks;
+ };
+
+ // Return empty string if transcoding files are not found
+ static std::string get_best_transcoding_audio_url(const Json::Value &media_json) {
+ const Json::Value &transcodings_json = media_json["transcodings"];
+ if(transcodings_json.isArray() && !transcodings_json.empty() && transcodings_json[0].isObject()) {
+ const Json::Value &transcoding_url = transcodings_json[0]["url"];
+ if(transcoding_url.isString())
+ return transcoding_url.asString();
+ }
+ return "";
+ }
+
+ static std::string duration_to_descriptive_string(int64_t seconds) {
+ seconds /= 1000;
+ time_t minutes = seconds / 60;
+ time_t hours = minutes / 60;
+
+ std::string str;
+ if(hours >= 1) {
+ str = std::to_string(hours) + " hour" + (hours == 1 ? "" : "s");
+ seconds -= (hours * 60 * 60);
+ }
+
+ if(minutes >= 1) {
+ if(!str.empty())
+ str += ", ";
+ str += std::to_string(minutes) + " minute" + (minutes == 1 ? "" : "s");
+ seconds -= (minutes * 60);
+ }
+
+ if(!str.empty() || seconds > 0) {
+ if(!str.empty())
+ str += ", ";
+ str += std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s");
+ }
+
+ return str;
+ }
+
+ static std::string collection_item_get_duration(const Json::Value &item_json) {
+ const Json::Value &full_duration_json = item_json["full_duration"];
+ if(full_duration_json.isInt64())
+ return duration_to_descriptive_string(full_duration_json.asInt64());
+
+ const Json::Value &duration_json = item_json["duration"];
+ if(duration_json.isInt64())
+ return duration_to_descriptive_string(duration_json.asInt64());
+
+ return "";
+ }
+
+ static std::shared_ptr<BodyItem> parse_collection_item(const Json::Value &item_json) {
+ std::string title;
+
+ const Json::Value &title_json = item_json["title"];
+ const Json::Value &username_json = item_json["username"];
+ if(title_json.isString())
+ title = title_json.asString();
+ else if(username_json.isString())
+ title = username_json.asString();
+ else
+ return nullptr;
+
+ auto body_item = BodyItem::create(std::move(title));
+ std::string description;
+
+ const Json::Value &media_json = item_json["media"];
+ if(media_json.isObject())
+ body_item->url = get_best_transcoding_audio_url(media_json);
+
+ if(body_item->url.empty()) {
+ const Json::Value &tracks_json = item_json["tracks"];
+ if(tracks_json.isArray()) {
+ auto playlist = std::make_shared<SoundcloudPlaylist>();
+ for(const Json::Value &track_json : tracks_json) {
+ if(!track_json.isObject())
+ continue;
+
+ auto track = parse_collection_item(track_json);
+ if(track)
+ playlist->tracks.push_back(std::move(track));
+ }
+
+ description = "Playlist with " + std::to_string(playlist->tracks.size()) + " track" + (playlist->tracks.size() == 1 ? "" : "s");
+ body_item->extra = std::move(playlist);
+ body_item->url = "track";
+ }
+ }
+
+ if(body_item->url.empty()) {
+ const Json::Value &id_json = item_json["id"];
+ if(id_json.isInt64())
+ body_item->url = "https://api-v2.soundcloud.com/stream/users/" + std::to_string(id_json.asInt64());
+ }
+
+ const Json::Value &artwork_url_json = item_json["artwork_url"];
+ const Json::Value &avatar_url_json = item_json["avatar_url"];
+ if(artwork_url_json.isString()) {
+ // For larger thumbnails
+ /*
+ if(strstr(artwork_url_json.asCString(), "-large") != 0) {
+ std::string artwork_url = artwork_url_json.asString();
+ string_replace_all(artwork_url, "-large", "-t200x200");
+ body_item->thumbnail_url = std::move(artwork_url);
+ body_item->thumbnail_size.x = 200;
+ body_item->thumbnail_size.y = 200;
+ } else {
+ body_item->thumbnail_url = artwork_url_json.asString();
+ body_item->thumbnail_size.x = 100;
+ body_item->thumbnail_size.y = 100;
+ }
+ */
+ body_item->thumbnail_url = artwork_url_json.asString();
+ body_item->thumbnail_size.x = 100;
+ body_item->thumbnail_size.y = 100;
+ } else if(avatar_url_json.isString()) {
+ body_item->thumbnail_url = avatar_url_json.asString();
+ body_item->thumbnail_size.x = 100;
+ body_item->thumbnail_size.y = 100;
+ body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE;
+ }
+
+ std::string duration_str = collection_item_get_duration(item_json);
+ if(!duration_str.empty()) {
+ if(!description.empty())
+ description += '\n';
+ description += std::move(duration_str);
+ }
+
+ body_item->set_description(std::move(description));
+ return body_item;
+ }
+
+ static PluginResult parse_user_page(const Json::Value &json_root, BodyItems &result_items, std::string &next_href) {
+ if(!json_root.isObject())
+ return PluginResult::ERR;
+
+ const Json::Value &next_href_json = json_root["next_href"];
+ if(next_href_json.isString())
+ next_href = next_href_json.asString();
+
+ const Json::Value &collection_json = json_root["collection"];
+ if(!collection_json.isArray())
+ return PluginResult::ERR;
+
+ for(const Json::Value &item_json : collection_json) {
+ if(!item_json.isObject())
+ continue;
+
+ const Json::Value &track_json = item_json["track"];
+ const Json::Value &playlist_json = item_json["playlist"];
+ if(track_json.isObject()) {
+ auto body_item = parse_collection_item(track_json);
+ if(body_item)
+ result_items.push_back(std::move(body_item));
+ } else if(playlist_json.isObject()) {
+ auto body_item = parse_collection_item(playlist_json);
+ if(body_item)
+ result_items.push_back(std::move(body_item));
+ }
+ }
+
+ return PluginResult::OK;
+ }
+
+ PluginResult SoundcloudPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) {
+ if(url.empty())
+ return PluginResult::ERR;
+
+ if(url == "track") {
+ auto body = create_body();
+ body->items = static_cast<SoundcloudPlaylist*>(submit_body_item->extra.get())->tracks;
+ result_tabs.push_back(Tab{std::move(body), std::make_unique<SoundcloudPlaylistPage>(program, title), nullptr});
+ } else if(url.find("/stream/users/") != std::string::npos) {
+ std::string query_url = url + "?client_id=" + client_id + "&limit=20&offset=0&linked_partitioning=1&app_version=1616689516&app_locale=en";
+
+ Json::Value json_root;
+ DownloadResult result = download_json(json_root, query_url, {}, true);
+ if(result != DownloadResult::OK) return download_result_to_plugin_result(result);
+
+ auto body = create_body();
+ std::string next_href;
+ PluginResult pr = parse_user_page(json_root, body->items, next_href);
+ if(pr != PluginResult::OK) return pr;
+
+ result_tabs.push_back(Tab{std::move(body), std::make_unique<SoundcloudUserPage>(program, title, url, std::move(next_href)), nullptr});
+ } else {
+ std::string query_url = url + "?client_id=" + client_id;
+
+ Json::Value json_root;
+ DownloadResult result = download_json(json_root, query_url, {}, true);
+ if(result != DownloadResult::OK) return download_result_to_plugin_result(result);
+
+ if(!json_root.isObject())
+ return PluginResult::ERR;
+
+ const Json::Value &url_json = json_root["url"];
+ if(!url_json.isString())
+ return PluginResult::ERR;
+
+ result_tabs.push_back(Tab{create_body(), std::make_unique<SoundcloudAudioPage>(program, url_json.asString()), nullptr});
+ }
+
+ return PluginResult::OK;
+ }
+
+ SearchResult SoundcloudSearchPage::search(const std::string &str, BodyItems &result_items) {
+ query_urn.clear();
+ PluginResult result = get_page(str, 0, result_items);
+ if(result != PluginResult::OK)
+ return SearchResult::ERR;
+ return SearchResult::OK;
+ }
+
+ PluginResult SoundcloudSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) {
+ std::string url = "https://api-v2.soundcloud.com/search?q=";
+ url += url_param_encode(str);
+ url += "&variant_ids=2227&facet=model&client_id=" + client_id + "&limit=20&offset=" + std::to_string(page * 20) + "&linked_partitioning=1&app_version=1616689516&app_locale=en";
+ if(!query_urn.empty())
+ url += "&query_url=" + url_param_encode(query_urn);
+ else if(page > 0)
+ return PluginResult::OK;
+
+ Json::Value json_root;
+ DownloadResult result = download_json(json_root, url, {}, true);
+ if(result != DownloadResult::OK) return download_result_to_plugin_result(result);
+
+ if(!json_root.isObject())
+ return PluginResult::ERR;
+
+ const Json::Value &query_urn_json = json_root["query_urn"];
+ if(query_urn_json.isString())
+ query_urn = query_urn_json.asString();
+
+ const Json::Value &collection_json = json_root["collection"];
+ if(!collection_json.isArray())
+ return PluginResult::ERR;
+
+ for(const Json::Value &item_json : collection_json) {
+ if(!item_json.isObject())
+ continue;
+
+ const Json::Value &kind_json = item_json["kind"];
+ if(!kind_json.isString())
+ continue;
+
+ if(strcmp(kind_json.asCString(), "user") == 0 || strcmp(kind_json.asCString(), "track") == 0 || strcmp(kind_json.asCString(), "playlist") == 0) {
+ auto body_item = parse_collection_item(item_json);
+ if(body_item)
+ result_items.push_back(std::move(body_item));
+ }
+ }
+
+ return PluginResult::OK;
+ }
+
+ PluginResult SoundcloudUserPage::get_page(const std::string&, int page, BodyItems &result_items) {
+ while(current_page < page) {
+ PluginResult plugin_result = get_continuation_page(result_items);
+ if(plugin_result != PluginResult::OK) return plugin_result;
+ ++current_page;
+ }
+ return PluginResult::OK;
+ }
+
+ PluginResult SoundcloudUserPage::get_continuation_page(BodyItems &result_items) {
+ if(next_href.empty())
+ return PluginResult::OK;
+
+ Json::Value json_root;
+ DownloadResult result = download_json(json_root, next_href + "&client_id=" + client_id, {}, true);
+ if(result != DownloadResult::OK) return download_result_to_plugin_result(result);
+ return parse_user_page(json_root, result_items, next_href);
+ }
+} \ No newline at end of file
diff --git a/src/plugins/Spotify.cpp b/src/plugins/Spotify.cpp
index 14f9831..f56ed6c 100644
--- a/src/plugins/Spotify.cpp
+++ b/src/plugins/Spotify.cpp
@@ -274,8 +274,8 @@ namespace QuickMedia {
return PluginResult::OK;
}
- PluginResult SpotifyEpisodeListPage::submit(const std::string &, const std::string &, std::vector<Tab> &result_tabs) {
- result_tabs.push_back(Tab{nullptr, std::make_unique<SpotifyAudioPage>(program), nullptr});
+ PluginResult SpotifyEpisodeListPage::submit(const std::string &, const std::string &url, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{nullptr, std::make_unique<SpotifyAudioPage>(program, url), nullptr});
return PluginResult::OK;
}
} \ No newline at end of file
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index 99227d5..3813068 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -572,7 +572,7 @@ namespace QuickMedia {
// 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...", 350)});
} else {
- result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program), nullptr});
+ result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, url), nullptr});
}
return PluginResult::OK;
}
@@ -1099,10 +1099,10 @@ namespace QuickMedia {
return PluginResult::OK;
}
- PluginResult YoutubeChannelPage::submit(const std::string&, const std::string&, std::vector<Tab> &result_tabs) {
+ PluginResult YoutubeChannelPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) {
if(url.empty())
return PluginResult::OK;
- result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program), nullptr});
+ result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, url), nullptr});
return PluginResult::OK;
}
@@ -1126,8 +1126,8 @@ namespace QuickMedia {
return PluginResult::OK;
}
- PluginResult YoutubeRelatedVideosPage::submit(const std::string&, const std::string&, std::vector<Tab> &result_tabs) {
- result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program), nullptr});
+ PluginResult YoutubeRelatedVideosPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program, url), nullptr});
return PluginResult::OK;
}