diff options
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | icons/spotify_launcher.png | bin | 7423 -> 0 bytes | |||
-rw-r--r-- | images/spotify_logo.png | bin | 10877 -> 0 bytes | |||
-rw-r--r-- | launcher/QuickMedia.desktop | 4 | ||||
-rw-r--r-- | launcher/QuickMedia_tabbed.desktop | 2 | ||||
-rw-r--r-- | plugins/Spotify.hpp | 49 | ||||
-rw-r--r-- | src/DownloadUtils.cpp | 8 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 8 | ||||
-rw-r--r-- | src/plugins/Spotify.cpp | 290 |
9 files changed, 12 insertions, 355 deletions
@@ -1,6 +1,6 @@ # QuickMedia A dmenu-inspired native client for web services. -Currently supported web services: `youtube`, `spotify (podcasts)`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao` and _others_.\ +Currently supported web services: `youtube`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao` and _others_.\ **Note:** file-manager is early in progress.\ Config data, including manga progress is stored under `$HOME/.config/quickmedia`.\ Cache is stored under `$HOME/.cache/quickmedia`. @@ -8,7 +8,7 @@ Cache is stored under `$HOME/.cache/quickmedia`. ``` usage: quickmedia <plugin> [--use-system-mpv-config] [--dir <directory>] [-e <window>] OPTIONS: - plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, spotify, soundcloud, nyaa.si, matrix, saucenao, file-manager or stdin + plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, file-manager or stdin --no-video Only play audio when playing a video. Disabled by default --use-system-mpv-config Use system mpv config instead of no config. Disabled by default --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default @@ -90,7 +90,7 @@ Note that at the moment, cached images will not be scaled with the dpi. Images d ### Optional `noto-fonts-cjk` needs to be installed to view chinese, japanese and korean characters.\ `mpv` needs to be installed to play videos.\ -`youtube-dl` needs to be installed to play youtube music/video or spotify podcasts.\ +`youtube-dl` needs to be installed to play youtube music/videos.\ `libnotify` which provides `notify-send` needs to be installed to show notifications (on Linux and other systems that uses d-bus notification system).\ [automedia](https://git.dec05eba.com/AutoMedia/) needs to be installed when tracking manga with `Ctrl + T`.\ `waifu2x-ncnn-vulkan` needs to be installed when using the `--upscale-images` or `--upscale-images-always` option.\ diff --git a/icons/spotify_launcher.png b/icons/spotify_launcher.png Binary files differdeleted file mode 100644 index b3abe61..0000000 --- a/icons/spotify_launcher.png +++ /dev/null diff --git a/images/spotify_logo.png b/images/spotify_logo.png Binary files differdeleted file mode 100644 index a389f79..0000000 --- a/images/spotify_logo.png +++ /dev/null diff --git a/launcher/QuickMedia.desktop b/launcher/QuickMedia.desktop index 2089dc8..89849db 100644 --- a/launcher/QuickMedia.desktop +++ b/launcher/QuickMedia.desktop @@ -2,7 +2,7 @@ Type=Application Name=QuickMedia GenericName=QuickMedia -Comment=A dmenu-inspired native client for web services. Currently supported web services: youtube, spotify (podcasts), soundcloud, nyaa.si, manganelo, mangatown, mangadex, 4chan, matrix and others +Comment=A dmenu-inspired native client for web services. Currently supported web services: youtube, soundcloud, nyaa.si, manganelo, mangatown, mangadex, 4chan, matrix and others Exec=quickmedia launcher Terminal=false -Keywords=4chan;manga;matrix;nyaa;torrent;soundcloud;spotify;podcast;youtube;music;quickmedia; +Keywords=4chan;manga;matrix;nyaa;torrent;soundcloud;podcast;youtube;music;quickmedia; diff --git a/launcher/QuickMedia_tabbed.desktop b/launcher/QuickMedia_tabbed.desktop index 409e84f..72100a4 100644 --- a/launcher/QuickMedia_tabbed.desktop +++ b/launcher/QuickMedia_tabbed.desktop @@ -5,4 +5,4 @@ GenericName=QuickMedia tabbed Comment=Launch QuickMedia with tabs Exec=tabbed -c -k quickmedia launcher -e Terminal=false -Keywords=4chan;manga;matrix;nyaa;torrent;soundcloud;spotify;podcast;youtube;music;quickmedia; +Keywords=4chan;manga;matrix;nyaa;torrent;soundcloud;podcast;youtube;music;quickmedia; diff --git a/plugins/Spotify.hpp b/plugins/Spotify.hpp deleted file mode 100644 index 66cc992..0000000 --- a/plugins/Spotify.hpp +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include "Page.hpp" - -namespace QuickMedia { - class SpotifyPage : public Page { - public: - SpotifyPage(Program *program); - virtual ~SpotifyPage() = default; - protected: - DownloadResult download_json_error_retry(Json::Value &json_root, const std::string &url, std::vector<CommandArg> additional_args); - private: - PluginResult update_token(); - private: - std::string access_token; - }; - - class SpotifyPodcastSearchPage : public SpotifyPage { - public: - SpotifyPodcastSearchPage(Program *program) : SpotifyPage(program) {} - const char* get_title() const override { return "Podcasts"; } - 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; - }; - - class SpotifyEpisodeListPage : public SpotifyPage { - public: - SpotifyEpisodeListPage(Program *program, const std::string &url) : SpotifyPage(program), url(url) {} - const char* get_title() const override { return "Episodes"; } - bool search_is_filter() override { return true; } - 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; - private: - std::string url; - }; - - class SpotifyAudioPage : public VideoPage { - public: - SpotifyAudioPage(Program *program, const std::string &url) : VideoPage(program), url(url) {} - const char* get_title() const override { return ""; } - std::unique_ptr<RelatedVideosPage> create_related_videos_page(Program *, const std::string &, const std::string &) override { return nullptr; } - std::unique_ptr<Page> 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/src/DownloadUtils.cpp b/src/DownloadUtils.cpp index b6d21b9..eb8fa63 100644 --- a/src/DownloadUtils.cpp +++ b/src/DownloadUtils.cpp @@ -223,9 +223,11 @@ namespace QuickMedia { } bool download_async_gui(const std::string &url, const std::string &file_manager_start_dir, bool use_youtube_dl, bool no_video) { - char quickmedia_path[PATH_MAX]; - if(readlink("/proc/self/exe", quickmedia_path, sizeof(quickmedia_path)) == -1) - strcpy(quickmedia_path, "quickmedia"); + // TODO: Fix this not working when installed to /usr/bin/quickmedia for some reason + //char quickmedia_path[PATH_MAX]; + //if(readlink("/proc/self/exe", quickmedia_path, sizeof(quickmedia_path)) == -1) + // strcpy(quickmedia_path, "quickmedia"); + const char *quickmedia_path = "quickmedia"; std::vector<const char*> args = { quickmedia_path, "download", "-u", url.c_str(), "--dir", file_manager_start_dir.c_str() }; if(use_youtube_dl) args.push_back("--youtube-dl"); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index c2dbf1c..db03aac 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -8,7 +8,6 @@ #include "../plugins/Fourchan.hpp" #include "../plugins/NyaaSi.hpp" #include "../plugins/Matrix.hpp" -#include "../plugins/Spotify.hpp" #include "../plugins/Soundcloud.hpp" #include "../plugins/FileManager.hpp" #include "../plugins/Pipe.hpp" @@ -68,7 +67,6 @@ static const std::pair<const char*, const char*> valid_plugins[] = { std::make_pair("readm", "readm_logo.png"), std::make_pair("manga", nullptr), std::make_pair("youtube", "yt_logo_rgb_dark_small.png"), - std::make_pair("spotify", "spotify_logo.png"), std::make_pair("soundcloud", "soundcloud_logo.png"), std::make_pair("pornhub", "pornhub_logo.png"), std::make_pair("spankbang", "spankbang_logo.png"), @@ -314,7 +312,7 @@ namespace QuickMedia { static void usage() { fprintf(stderr, "usage: quickmedia <plugin> [--no-video] [--use-system-mpv-config] [--dir <directory>] [-e <window>]\n"); fprintf(stderr, "OPTIONS:\n"); - fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, spotify, soundcloud, nyaa.si, matrix, saucenao, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n"); + fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n"); fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n"); fprintf(stderr, " --use-system-mpv-config Use system mpv config instead of no config. Disabled by default\n"); fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n"); @@ -1020,7 +1018,6 @@ namespace QuickMedia { pipe_body->items.push_back(create_launcher_body_item("Nyaa.si", "nyaa.si", resources_root + "icons/nyaa_si_launcher.png")); pipe_body->items.push_back(create_launcher_body_item("SauceNAO", "saucenao", "")); pipe_body->items.push_back(create_launcher_body_item("Soundcloud", "soundcloud", resources_root + "icons/soundcloud_launcher.png")); - pipe_body->items.push_back(create_launcher_body_item("Spotify", "spotify", resources_root + "icons/spotify_launcher.png")); pipe_body->items.push_back(create_launcher_body_item("YouTube", "youtube", resources_root + "icons/yt_launcher.png")); pipe_body->items.push_back(create_launcher_body_item("YouTube (audio only)", "youtube-audio", resources_root + "icons/yt_launcher.png")); tabs.push_back(Tab{std::move(pipe_body), std::make_unique<PipePage>(this, "Select plugin to launch"), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); @@ -1148,9 +1145,6 @@ namespace QuickMedia { auto search_page = std::make_unique<MediaGenericSearchPage>(this, "https://xhamster.com/", sf::Vector2i(240, 135)); add_xhamster_handlers(search_page.get()); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 500)}); - } else if(strcmp(plugin_name, "spotify") == 0) { - tabs.push_back(Tab{create_body(), std::make_unique<SpotifyPodcastSearchPage>(this), create_search_bar("Search...", 350)}); - no_video = true; } else if(strcmp(plugin_name, "soundcloud") == 0) { tabs.push_back(Tab{create_body(), std::make_unique<SoundcloudSearchPage>(this), create_search_bar("Search...", 500)}); no_video = true; diff --git a/src/plugins/Spotify.cpp b/src/plugins/Spotify.cpp deleted file mode 100644 index d41446b..0000000 --- a/src/plugins/Spotify.cpp +++ /dev/null @@ -1,290 +0,0 @@ -#include "../../plugins/Spotify.hpp" -#include "../../include/NetUtils.hpp" -#include "../../include/Utils.hpp" -#include "../../include/Scale.hpp" - -namespace QuickMedia { - SpotifyPage::SpotifyPage(Program *program) : Page(program) { - Path spotify_cache_path = get_cache_dir().join("spotify").join("access_token"); - if(file_get_content(spotify_cache_path, access_token) != 0) - access_token.clear(); - } - - DownloadResult SpotifyPage::download_json_error_retry(Json::Value &json_root, const std::string &url, std::vector<CommandArg> additional_args) { - if(access_token.empty()) { - PluginResult update_token_res = update_token(); - if(update_token_res != PluginResult::OK) return DownloadResult::ERR; - } - - std::string authorization = "authorization: Bearer " + access_token; - additional_args.push_back({ "-H", authorization.c_str() }); - - std::string err_msg; - DownloadResult result = download_json(json_root, url, additional_args, true, &err_msg); - if(result != DownloadResult::OK) return result; - - if(!json_root.isObject()) - return DownloadResult::ERR; - - const Json::Value &error_json = json_root["error"]; - if(error_json.isObject()) { - const Json::Value &status_json = error_json["status"]; - if(status_json.isInt() && status_json.asInt() == 401) { - fprintf(stderr, "Spotify access token expired, requesting a new token...\n"); - PluginResult update_token_res = update_token(); - if(update_token_res != PluginResult::OK) return DownloadResult::ERR; - - authorization = "authorization: Bearer " + access_token; - additional_args.back().value = authorization.c_str(); - DownloadResult result = download_json(json_root, url, additional_args, true); - if(result != DownloadResult::OK) return result; - - if(!json_root.isObject()) - return DownloadResult::ERR; - } - } - - return DownloadResult::OK; - } - - PluginResult SpotifyPage::update_token() { - std::string url = "https://open.spotify.com/get_access_token?reason=transport&productType=web_player"; - - std::vector<CommandArg> additional_args = { - { "-H", "authority: application/json" }, - { "-H", "Referer: open.spotify.com" }, - { "-H", "app-platform: WebPlayer" }, - { "-H", "spotify-app-version: 1.1.53.594.g8178ce9b" } - }; - - Json::Value json_root; - DownloadResult result = download_json(json_root, url, std::move(additional_args), true); - if(result != DownloadResult::OK) return download_result_to_plugin_result(result); - - if(!json_root.isObject()) - return PluginResult::ERR; - - const Json::Value &access_token_json = json_root["accessToken"]; - if(!access_token_json.isString()) - return PluginResult::ERR; - - access_token = access_token_json.asString(); - Path spotify_cache_dir = get_cache_dir().join("spotify"); - if(create_directory_recursive(spotify_cache_dir) == 0) - fprintf(stderr, "Failed to create spotify cache directory\n"); - spotify_cache_dir.join("access_token"); - file_overwrite_atomic(spotify_cache_dir, access_token); - return PluginResult::OK; - } - - static void image_sources_set_thumbnail(BodyItem *body_item, const Json::Value &sources_list_json) { - if(!sources_list_json.isArray()) - return; - - for(const Json::Value &source_json : sources_list_json) { - if(!source_json.isObject()) - continue; - - const Json::Value &width_json = source_json["width"]; - const Json::Value &height_json = source_json["height"]; - const Json::Value &url_json = source_json["url"]; - if(!width_json.isInt() || !height_json.isInt() || !url_json.isString()) - continue; - - if(height_json.asInt() >= 200 && height_json.asInt() <= 350) { - body_item->thumbnail_url = url_json.asString(); - body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; - body_item->thumbnail_size.x = width_json.asInt(); - body_item->thumbnail_size.y = height_json.asInt(); - body_item->thumbnail_size = clamp_to_size(body_item->thumbnail_size, sf::Vector2i(150, 150)); - return; - } - } - } - - SearchResult SpotifyPodcastSearchPage::search(const std::string &str, BodyItems &result_items) { - PluginResult result = get_page(str, 0, result_items); - if(result != PluginResult::OK) - return SearchResult::ERR; - return SearchResult::OK; - } - - PluginResult SpotifyPodcastSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { - std::string url = "https://api.spotify.com/v1/search?query="; - url += url_param_encode(str); - url += "&type=show&include_external=audio&market=US&offset=" + std::to_string(page * 10) + "&limit=10"; - - std::vector<CommandArg> additional_args = { - { "-H", "accept: application/json" }, - { "-H", "Referer: https://open.spotify.com/" }, - { "-H", "app-platform: WebPlayer" }, - { "-H", "spotify-app-version: 1.1.53.594.g8178ce9b" } - }; - - Json::Value json_root; - DownloadResult result = download_json_error_retry(json_root, url, additional_args); - if(result != DownloadResult::OK) return download_result_to_plugin_result(result); - - if(!json_root.isObject()) - return PluginResult::ERR; - - const Json::Value &shows_json = json_root["shows"]; - if(!shows_json.isObject()) - return PluginResult::ERR; - - const Json::Value &items_json = shows_json["items"]; - if(!items_json.isArray()) - return PluginResult::ERR; - - for(const Json::Value &item_json : items_json) { - if(!item_json.isObject()) - continue; - - const Json::Value &name_json = item_json["name"]; - const Json::Value &uri_json = item_json["uri"]; - if(!name_json.isString() || !uri_json.isString()) - continue; - - auto body_item = BodyItem::create(name_json.asString()); - body_item->url = uri_json.asString(); - - const Json::Value &publisher_json = item_json["publisher"]; - if(publisher_json.isString()) { - body_item->set_description(publisher_json.asString()); - body_item->set_description_color(sf::Color(179, 179, 179)); - } - - image_sources_set_thumbnail(body_item.get(), item_json["images"]); - result_items.push_back(std::move(body_item)); - } - - return PluginResult::OK; - } - - PluginResult SpotifyPodcastSearchPage::submit(const std::string &, const std::string &url, std::vector<Tab> &result_tabs) { - auto body = create_body(); - auto episode_list_page = std::make_unique<SpotifyEpisodeListPage>(program, url); - PluginResult result = episode_list_page->get_page("", 0, body->items); - if(result != PluginResult::OK) - return result; - - result_tabs.push_back(Tab{std::move(body), std::move(episode_list_page), nullptr}); - return result; - } - - static std::string unix_time_to_local_time_str(time_t unix_time) { - struct tm time_tm; - localtime_r(&unix_time, &time_tm); - char time_str[128] = {0}; - strftime(time_str, sizeof(time_str) - 1, "%Y-%m-%d %H:%M", &time_tm); - return time_str; - } - - PluginResult SpotifyEpisodeListPage::get_page(const std::string &, int page, BodyItems &result_items) { - std::string request_url = "https://api-partner.spotify.com/pathfinder/v1/query?operationName=queryShowEpisodes&variables="; - request_url += url_param_encode("{\"uri\":\"" + url + "\",\"offset\":" + std::to_string(page * 50) + ",\"limit\":50}"); - request_url += "&extensions="; - request_url += url_param_encode("{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"e0e5ce27bd7748d2c59b4d44ba245a8992a05be75d6fabc3b20753fc8857444d\"}}"); - - std::vector<CommandArg> additional_args = { - { "-H", "authority: api-partner.spotify.com" }, - { "-H", "accept: application/json" }, - { "-H", "Referer: https://open.spotify.com/" }, - { "-H", "app-platform: WebPlayer" }, - { "-H", "spotify-app-version: 1.1.54.40.g75ab4382" } - }; - - Json::Value json_root; - DownloadResult result = download_json_error_retry(json_root, request_url, additional_args); - if(result != DownloadResult::OK) return download_result_to_plugin_result(result); - - if(!json_root.isObject()) - return PluginResult::ERR; - - const Json::Value &data_json = json_root["data"]; - if(!data_json.isObject()) - return PluginResult::ERR; - - const Json::Value &podcast_json = data_json["podcast"]; - if(!podcast_json.isObject()) - return PluginResult::ERR; - - const Json::Value &episodes_json = podcast_json["episodes"]; - if(!episodes_json.isObject()) - return PluginResult::ERR; - - const Json::Value &episode_items_json = episodes_json["items"]; - if(!episode_items_json.isArray()) - return PluginResult::ERR; - - for(const Json::Value &episode_item_json : episode_items_json) { - if(!episode_item_json.isObject()) - continue; - - const Json::Value &episode_json = episode_item_json["episode"]; - if(!episode_json.isObject()) - continue; - - const Json::Value &name_json = episode_json["name"]; - if(!name_json.isString()) - continue; - - const Json::Value &sharing_info_json = episode_json["sharingInfo"]; - if(!sharing_info_json.isObject()) - continue; - - const Json::Value &share_url_json = sharing_info_json["shareUrl"]; - if(!share_url_json.isString()) - continue; - - auto body_item = BodyItem::create(name_json.asString()); - body_item->url = share_url_json.asString(); - - std::string description; - std::string time; - - const Json::Value &description_json = episode_json["description"]; - if(description_json.isString()) - description += description_json.asString(); - - const Json::Value &release_data_json = episode_json["releaseDate"]; - if(release_data_json.isObject()) { - const Json::Value &iso_string_json = release_data_json["isoString"]; - if(iso_string_json.isString()) - time += unix_time_to_local_time_str(iso_utc_to_unix_time(iso_string_json.asCString())); - } - - const Json::Value &duration_json = episode_json["duration"]; - if(duration_json.isObject()) { - const Json::Value &total_ms_json = duration_json["totalMilliseconds"]; - if(total_ms_json.isInt()) { - if(!time.empty()) - time += " • "; - time += std::to_string(total_ms_json.asInt() / 1000 / 60) + " min"; - } - } - - if(!time.empty()) { - if(!description.empty()) - description += '\n'; - description += std::move(time); - } - - body_item->set_description(std::move(description)); - body_item->set_description_color(sf::Color(179, 179, 179)); - - const Json::Value &cover_art_json = episode_json["coverArt"]; - if(cover_art_json.isObject()) - image_sources_set_thumbnail(body_item.get(), cover_art_json["sources"]); - - result_items.push_back(std::move(body_item)); - } - - return PluginResult::OK; - } - - 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 |