#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 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 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 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 &result_tabs) { auto body = create_body(); auto episode_list_page = std::make_unique(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 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 &result_tabs) { result_tabs.push_back(Tab{nullptr, std::make_unique(program, url), nullptr}); return PluginResult::OK; } }