From da827778f8c5d2f0cfc56b297099ba58454c38ed Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 26 Mar 2021 16:45:41 +0100 Subject: Add soundcloud --- README.md | 4 +- TODO | 3 +- icons/soundcloud_launcher.png | Bin 0 -> 4101 bytes images/soundcloud_logo.png | Bin 0 -> 5951 bytes include/Body.hpp | 7 + include/QuickMedia.hpp | 2 +- launcher/QuickMedia-soundcloud.desktop | 9 ++ plugins/ImageBoard.hpp | 2 + plugins/Matrix.hpp | 2 + plugins/Page.hpp | 2 + plugins/Pornhub.hpp | 5 +- plugins/Soundcloud.hpp | 56 +++++++ plugins/Spotify.hpp | 5 +- plugins/Youtube.hpp | 4 +- src/Body.cpp | 1 + src/QuickMedia.cpp | 42 +++-- src/plugins/Pornhub.cpp | 8 +- src/plugins/Soundcloud.cpp | 287 +++++++++++++++++++++++++++++++++ src/plugins/Spotify.cpp | 4 +- src/plugins/Youtube.cpp | 10 +- 20 files changed, 422 insertions(+), 31 deletions(-) create mode 100644 icons/soundcloud_launcher.png create mode 100644 images/soundcloud_logo.png create mode 100644 launcher/QuickMedia-soundcloud.desktop create mode 100644 plugins/Soundcloud.hpp create mode 100644 src/plugins/Soundcloud.cpp diff --git a/README.md b/README.md index 5c2436a..771d1d4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # QuickMedia A dmenu-inspired native client for web services. -Currently supported web services: `youtube`, `spotify (podcasts)`, `nyaa.si`, `manganelo`, `mangatown`, `mangadex`, `4chan`, `matrix` and _others_.\ +Currently supported web services: `youtube`, `spotify (podcasts)`, `soundcloud`, `nyaa.si`, `manganelo`, `mangatown`, `mangadex`, `4chan`, `matrix` and _others_.\ **Note:** Manganelo doesn't work when used with TOR.\ **Note:** Posting comments on 4chan doesn't work when used with TOR. However browsing works.\ **Note:** TOR system service needs to be running (`systemctl start tor.service`) when using `--tor` option.\ @@ -11,7 +11,7 @@ Cache is stored under `$HOME/.cache/quickmedia`. ``` usage: QuickMedia [--tor] [--use-system-mpv-config] [--dir ] OPTIONS: - plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, youtube, spotify, nyaa.si, matrix, file-manager or pipe + plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, youtube, spotify, soundcloud, nyaa.si, matrix, file-manager or pipe --no-video Only play audio when playing a video. Disabled by default --tor Use tor. Disabled by default --use-system-mpv-config Use system mpv config instead of no config. Disabled by default diff --git a/TODO b/TODO index 827a84e..fe5b750 100644 --- a/TODO +++ b/TODO @@ -158,4 +158,5 @@ Sort reactions by timestamp. Check what happens with xsrf_token if comments are not fetched for a long time. Does it time out? if so do we need to refetch the video page to get the new token?. Add support for comments in live youtube videos, api is at: https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8. Make video visible when reading comments (youtube). -Convert nyaa.si date from unix date to local time. \ No newline at end of file +Convert nyaa.si date from unix date to local time. +When ui is scaled then the predicated thumbnail size will be wrong since its scaled in Body but not in the plugins where they are requested. \ No newline at end of file diff --git a/icons/soundcloud_launcher.png b/icons/soundcloud_launcher.png new file mode 100644 index 0000000..baa1840 Binary files /dev/null and b/icons/soundcloud_launcher.png differ diff --git a/images/soundcloud_logo.png b/images/soundcloud_logo.png new file mode 100644 index 0000000..a314bb3 Binary files /dev/null and b/images/soundcloud_logo.png differ diff --git a/include/Body.hpp b/include/Body.hpp index 5e68b5b..cc11664 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -33,6 +33,12 @@ namespace QuickMedia { CIRCLE }; + // TODO: Remove and create an Userdata class instead to replace the void* userdata in BodyItem + class BodyItemExtra { + public: + virtual ~BodyItemExtra() = default; + }; + struct Reaction { std::unique_ptr text; void *userdata = nullptr; @@ -146,6 +152,7 @@ namespace QuickMedia { ThumbnailMaskType thumbnail_mask_type = ThumbnailMaskType::NONE; sf::Vector2i thumbnail_size; std::vector reactions; // TODO: Move to a different body item type + std::shared_ptr extra; // TODO: Remove private: // TODO: Clean up these strings when set in text, and get_title for example should return |title_text.getString()| // TODO: Use sf::String instead, removes the need to convert to utf32 every time the text is dirty (for example when resizing window) diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 21b451d..51a97b4 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -101,7 +101,7 @@ namespace QuickMedia { void page_loop_render(sf::RenderWindow &window, std::vector &tabs, int selected_tab, TabAssociatedData &tab_associated_data, const Json::Value *json_chapters); using PageLoopSubmitHandler = std::function &new_tabs)>; void page_loop(std::vector &tabs, int start_tab_index = 0, PageLoopSubmitHandler after_submit_handler = nullptr); - void video_content_page(VideoPage *video_page, std::string video_url, std::string video_title, bool download_if_streaming_fails); + void video_content_page(VideoPage *video_page, std::string video_title, bool download_if_streaming_fails); // Returns -1 to go to previous chapter, 0 to stay on same chapter and 1 to go to next chapter int image_page(MangaImagesPage *images_page, Body *chapters_body); void image_continuous_page(MangaImagesPage *images_page); diff --git a/launcher/QuickMedia-soundcloud.desktop b/launcher/QuickMedia-soundcloud.desktop new file mode 100644 index 0000000..2b25fec --- /dev/null +++ b/launcher/QuickMedia-soundcloud.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=QuickMedia Soundcloud +GenericName=Soundcloud player +Comment=Soundcloud music player +Icon=/usr/share/quickmedia/icons/soundcloud_launcher.png +Exec=QuickMedia soundcloud --no-video +Terminal=false +Keywords=soundcloud;player;quickmedia;music; diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp index c65b269..bd47bec 100644 --- a/plugins/ImageBoard.hpp +++ b/plugins/ImageBoard.hpp @@ -21,6 +21,7 @@ namespace QuickMedia { std::unique_ptr create_channels_page(Program*, const std::string&) override { return nullptr; } + std::string get_url() override { return video_url; } virtual PluginResult login(const std::string &token, const std::string &pin, std::string &response_msg); virtual PostResult post_comment(const std::string &captcha_id, const std::string &comment) = 0; virtual const std::string& get_pass_id(); @@ -28,5 +29,6 @@ namespace QuickMedia { const std::string board_id; const std::string thread_id; const std::vector cached_media_urls; + std::string video_url; }; } \ No newline at end of file diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 7028e4d..26ad926 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -420,6 +420,8 @@ namespace QuickMedia { std::unique_ptr create_channels_page(Program*, const std::string&) override { return nullptr; } + std::string get_url() override { return url; } + std::string url; }; class MatrixChatPage : public Page { diff --git a/plugins/Page.hpp b/plugins/Page.hpp index 44526db..f1ef893 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -63,6 +63,7 @@ namespace QuickMedia { virtual sf::Vector2i get_thumbnail_max_size() { return sf::Vector2i(480, 360); }; Program *program; + std::shared_ptr submit_body_item; // TODO: Remove this }; enum class TrackResult { @@ -106,5 +107,6 @@ namespace QuickMedia { virtual std::unique_ptr create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) = 0; // Return nullptr if the service doesn't support channels page virtual std::unique_ptr create_channels_page(Program *program, const std::string &channel_url) = 0; + virtual std::string get_url() = 0; }; } \ No newline at end of file diff --git a/plugins/Pornhub.hpp b/plugins/Pornhub.hpp index 5c3f835..87e33da 100644 --- a/plugins/Pornhub.hpp +++ b/plugins/Pornhub.hpp @@ -21,7 +21,7 @@ namespace QuickMedia { class PornhubVideoPage : public VideoPage { public: - PornhubVideoPage(Program *program) : VideoPage(program) {} + PornhubVideoPage(Program *program, const std::string &url) : VideoPage(program), url(url) {} const char* get_title() const override { return ""; } BodyItems get_related_media(const std::string &url, std::string &channel_url) override; std::unique_ptr create_search_page(Program *program, int &search_delay) override; @@ -29,5 +29,8 @@ namespace QuickMedia { std::unique_ptr create_channels_page(Program*, const std::string&) override { return nullptr; } + std::string get_url() override { return url; } + private: + std::string url; }; } \ No newline at end of file diff --git a/plugins/Soundcloud.hpp b/plugins/Soundcloud.hpp new file mode 100644 index 0000000..4962c04 --- /dev/null +++ b/plugins/Soundcloud.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "Page.hpp" + +namespace QuickMedia { + class SoundcloudPage : public Page { + public: + SoundcloudPage(Program *program) : Page(program) {} + virtual ~SoundcloudPage() = default; + PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; + }; + + class SoundcloudSearchPage : public SoundcloudPage { + public: + SoundcloudSearchPage(Program *program) : SoundcloudPage(program) {} + const char* get_title() const override { return "Search"; } + 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; + private: + std::string query_urn; + }; + + class SoundcloudUserPage : public SoundcloudPage { + public: + SoundcloudUserPage(Program *program, const std::string &username, const std::string &userpage_url, std::string next_href) : SoundcloudPage(program), username(username), userpage_url(userpage_url), next_href(std::move(next_href)), current_page(0) {} + const char* get_title() const override { return username.c_str(); } + PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; + private: + PluginResult get_continuation_page(BodyItems &result_items); + private: + std::string username; + std::string userpage_url; + std::string next_href; + int current_page; + }; + + class SoundcloudPlaylistPage : public SoundcloudPage { + public: + SoundcloudPlaylistPage(Program *program, const std::string &playlist_name) : SoundcloudPage(program), playlist_name(playlist_name) {} + const char* get_title() const override { return playlist_name.c_str(); } + private: + std::string playlist_name; + }; + + class SoundcloudAudioPage : public VideoPage { + public: + SoundcloudAudioPage(Program *program, const std::string &url) : VideoPage(program), url(url) {} + const char* get_title() const override { return ""; } + std::unique_ptr create_related_videos_page(Program *, const std::string &, const std::string &) override { return nullptr; } + std::unique_ptr create_channels_page(Program *, const std::string &) override { return nullptr; } + std::string get_url() override { return url; } + private: + std::string url; + }; +} \ No newline at end of file diff --git a/plugins/Spotify.hpp b/plugins/Spotify.hpp index 9cdd2af..89f8f3d 100644 --- a/plugins/Spotify.hpp +++ b/plugins/Spotify.hpp @@ -38,9 +38,12 @@ namespace QuickMedia { class SpotifyAudioPage : public VideoPage { public: - SpotifyAudioPage(Program *program) : VideoPage(program) {} + SpotifyAudioPage(Program *program, const std::string &url) : VideoPage(program), url(url) {} const char* get_title() const override { return ""; } std::unique_ptr create_related_videos_page(Program *, const std::string &, const std::string &) override { return nullptr; } std::unique_ptr create_channels_page(Program *, const std::string &) override { return nullptr; } + std::string get_url() override { return url; } + private: + std::string url; }; } \ No newline at end of file diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index f8a5d5f..4691f04 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -75,15 +75,17 @@ namespace QuickMedia { class YoutubeVideoPage : public VideoPage { public: - YoutubeVideoPage(Program *program) : VideoPage(program) {} + YoutubeVideoPage(Program *program, const std::string &url) : VideoPage(program), url(url) {} const char* get_title() const override { return ""; } BodyItems get_related_media(const std::string &url, std::string &channel_url) override; std::unique_ptr create_search_page(Program *program, int &search_delay) override; std::unique_ptr create_comments_page(Program *program) override; std::unique_ptr create_related_videos_page(Program *program, const std::string &video_url, const std::string &video_title) override; std::unique_ptr create_channels_page(Program *program, const std::string &channel_url) override; + std::string get_url() override { return url; } private: std::string xsrf_token; std::string comments_continuation_token; + std::string url; }; } \ No newline at end of file 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 [--tor] [--no-video] [--use-system-mpv-config] [--dir ]\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(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(this), create_search_bar("Search...", 500)}); } else if(strcmp(plugin_name, "mastodon") == 0 || strcmp(plugin_name, "pleroma") == 0) { auto pleroma = std::make_shared(); auto search_body = create_body(); @@ -1106,11 +1114,12 @@ namespace QuickMedia { std::function 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 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(new_tabs[0].page.get()); @@ -1188,9 +1200,9 @@ namespace QuickMedia { image_board_thread_page(static_cast(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(new_tabs[0].page.get()), selected_item->url, selected_item->get_title(), false); + video_content_page(static_cast(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 &result_tabs) { - result_tabs.push_back(Tab{nullptr, std::make_unique(program), nullptr}); + PluginResult PornhubSearchPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back(Tab{nullptr, std::make_unique(program, url), nullptr}); return PluginResult::OK; } - PluginResult PornhubRelatedVideosPage::submit(const std::string&, const std::string&, std::vector &result_tabs) { - result_tabs.push_back(Tab{nullptr, std::make_unique(program), nullptr}); + PluginResult PornhubRelatedVideosPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back(Tab{nullptr, std::make_unique(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 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(); + 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 &result_tabs) { + if(url.empty()) + return PluginResult::ERR; + + if(url == "track") { + auto body = create_body(); + body->items = static_cast(submit_body_item->extra.get())->tracks; + result_tabs.push_back(Tab{std::move(body), std::make_unique(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(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(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 &result_tabs) { - result_tabs.push_back(Tab{nullptr, std::make_unique(program), nullptr}); + PluginResult SpotifyEpisodeListPage::submit(const std::string &, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back(Tab{nullptr, std::make_unique(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(program, url, "", title), create_search_bar("Search...", 350)}); } else { - result_tabs.push_back(Tab{nullptr, std::make_unique(program), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique(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 &result_tabs) { + PluginResult YoutubeChannelPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { if(url.empty()) return PluginResult::OK; - result_tabs.push_back(Tab{nullptr, std::make_unique(program), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique(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 &result_tabs) { - result_tabs.push_back(Tab{nullptr, std::make_unique(program), nullptr}); + PluginResult YoutubeRelatedVideosPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back(Tab{nullptr, std::make_unique(program, url), nullptr}); return PluginResult::OK; } -- cgit v1.2.3