From da827778f8c5d2f0cfc56b297099ba58454c38ed Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 26 Mar 2021 16:45:41 +0100 Subject: Add soundcloud --- 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 +- 6 files changed, 328 insertions(+), 24 deletions(-) create mode 100644 src/plugins/Soundcloud.cpp (limited to 'src') 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