Reorganization of code to improve readability and also fix Spotify-hosted podcasts
This commit is contained in:
parent
a4e8e2a360
commit
576afb08f5
@ -2,9 +2,15 @@
|
||||
#include "Utils.h"
|
||||
#include "tiny-AES-c/aes.h"
|
||||
|
||||
const uint8_t IV[] = { 0x72, 0xE0, 0x67, 0xFB, 0xDD, 0xCB, 0xCF, 0x77, 0xEB, 0xE8, 0xBC, 0x64, 0x3F, 0x63, 0x0D, 0x93 };
|
||||
const std::string urlRegex = "https?:\\/\\/(?:www\.)?([-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})"
|
||||
constexpr BYTE IV[] = {0x72, 0xE0, 0x67, 0xFB, 0xDD, 0xCB, 0xCF, 0x77, 0xEB, 0xE8, 0xBC, 0x64, 0x3F, 0x63, 0x0D, 0x93};
|
||||
constexpr char podcastKey[] = "\xde" "\xad" "\xbe" "\xef" "\xde" "\xad" "\xbe" "\xef" "\xde" "\xad" "\xbe" "\xef" "\xde"
|
||||
"\xad" "\xbe" "\xef";
|
||||
constexpr BYTE nullKey[] = "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0";
|
||||
|
||||
static const std::string urlRegex = "https?:\\/\\/(?:www\.)?([-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})"
|
||||
"\\b([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)";
|
||||
static const std::string albumSearchPattern = "\x68\x65\x69\x67\x68\x74\x22\x20\x3A\x20\x36\x34\x30";
|
||||
static const std::wstring songDirRoot = L"Downloads";
|
||||
|
||||
bool Utils::Detour32(char* src, char* dst, const intptr_t len)
|
||||
{
|
||||
@ -254,55 +260,62 @@ int GetUrlNum(int quality)
|
||||
}
|
||||
}
|
||||
|
||||
static const std::string albumSearchPattern = "\x68\x65\x69\x67\x68\x74\x22\x20\x3A\x20\x36\x34\x30";
|
||||
static const std::wstring songDirRoot = L"Downloads";
|
||||
static std::wstring songDir = songDirRoot;
|
||||
void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, std::string authToken, int quality)
|
||||
std::string AttemptDecryption(std::string data, std::string key)
|
||||
{
|
||||
std::string downloadStr;
|
||||
std::wstring songExtension = L".ogg";
|
||||
// Decrypt encrypted data with Tiny AES in C
|
||||
struct AES_ctx ctx;
|
||||
AES_init_ctx_iv(&ctx, reinterpret_cast<const uint8_t*>(&key[0]), IV);
|
||||
AES_CTR_xcrypt_buffer(&ctx, reinterpret_cast<uint8_t*>(&data[0]), data.size());
|
||||
|
||||
SongInfo* songInfo = new SongInfo();
|
||||
/*songInfo->title = "";
|
||||
songInfo->artist = "";
|
||||
songInfo->album = "";
|
||||
songInfo->coverUrl = "";
|
||||
songInfo->fileType = FileType::OGG;*/
|
||||
// Check if decrypted song data has valid header (assume Ogg)
|
||||
if (data.length() < 4 || data.compare(0, 4, "OggS") != 0)
|
||||
return "Error: Not a valid Ogg file after decryption! Please try again";
|
||||
|
||||
if (fileId.empty() || uri.empty() || authToken.empty())
|
||||
{
|
||||
std::cout << "Could not download song or episode: missing fileId, trackUri, or authToken!" << std::endl;
|
||||
return;
|
||||
return data;
|
||||
}
|
||||
|
||||
// Is length check even needed for compare()?
|
||||
if (uri.length() > 13 && uri.compare(0, 13, "spotify:track") == 0)
|
||||
std::string DownloadAudioData(std::string fileId, std::string uri, std::string key, std::string authToken, int quality)
|
||||
{
|
||||
std::cout << "Downloading song..." << std::endl;
|
||||
std::string audioData;
|
||||
std::string songHost, songPath;
|
||||
bool isSpotifyHosted;
|
||||
|
||||
if (key.empty())
|
||||
// Determine if we need to decrypt the download by parsing fileId for a URL
|
||||
try
|
||||
{
|
||||
std::cout << "Could not download song: missing key!";
|
||||
return;
|
||||
std::regex re(urlRegex);
|
||||
std::smatch match;
|
||||
if (std::regex_search(fileId, match, re) && match.size() > 1)
|
||||
{
|
||||
// fileId is valid URL (not hosted by Spotify)
|
||||
isSpotifyHosted = false;
|
||||
songHost = match.str(1);
|
||||
songPath = match.str(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// fileId is not valid URL (hosted by Spotify)
|
||||
isSpotifyHosted = true;
|
||||
}
|
||||
}
|
||||
catch (std::regex_error& e)
|
||||
{
|
||||
// Syntax error in the regular expression
|
||||
return "Error: Invalid regex while parsing fileID!";
|
||||
}
|
||||
|
||||
if (isSpotifyHosted)
|
||||
{
|
||||
// Get storage resolve from Spotify
|
||||
std::string urlNum = std::to_string(GetUrlNum(quality));
|
||||
std::string srStr = DownloadSpotifyUrl("spclient.wg.spotify.com",
|
||||
std::string srStr = Utils::DownloadSpotifyUrl("spclient.wg.spotify.com",
|
||||
"/storage-resolve/files/audio/interactive_prefetch/" + fileId + "?product=0", authToken);
|
||||
//"/storage-resolve/v2/files/audio/interactive/" + urlNum + "/" + fileId + "?product=0", authToken);
|
||||
|
||||
if (srStr.length() <= 5)
|
||||
{
|
||||
std::cout << "Error: Couldn't fetch storage resolve: (" + srStr + ")"<< std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (srStr.substr(0, 5).compare("Error") == 0)
|
||||
{
|
||||
std::cout << srStr << std::endl;
|
||||
return;
|
||||
}
|
||||
return "Error: Couldn't fetch storage resolve: (" + srStr + ")";
|
||||
else if (srStr.substr(0, 5).compare("Error") == 0)
|
||||
return srStr;
|
||||
|
||||
// Parse storage resolve response to get the encrypted song data's URL
|
||||
std::string songHost;
|
||||
@ -318,51 +331,105 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: Download URL not found" << std::endl;
|
||||
return;
|
||||
return "Error: Download URL not found";
|
||||
}
|
||||
}
|
||||
catch (std::regex_error& e)
|
||||
{
|
||||
// Syntax error in the regular expression
|
||||
std::cout << "Error: Invalid regex!" << std::endl;
|
||||
return;
|
||||
return "Error: Invalid regex!";
|
||||
}
|
||||
|
||||
// Download encrypted song data from Spotify
|
||||
downloadStr = DownloadSpotifyUrl(songHost, songPath, "");
|
||||
// Download encrypted audio data from Spotify
|
||||
audioData = Utils::DownloadSpotifyUrl(songHost, songPath, "");
|
||||
|
||||
if (downloadStr.length() <= 6)
|
||||
// Decrypt encrypted audio data using key
|
||||
std::string decAudioData = AttemptDecryption(audioData, key);
|
||||
|
||||
if (decAudioData.compare(0, 5, "Error") == 0)
|
||||
{
|
||||
std::cout << "Error: Could not download audio!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (downloadStr.compare(0, 6, "<HTML>") == 0)
|
||||
// Only try again on podcasts
|
||||
if (uri.length() > 15 && uri.compare(0, 15, "spotify:episode") == 0)
|
||||
{
|
||||
std::cout << "Error: " + downloadStr << std::endl;
|
||||
return;
|
||||
std::cout << "Podcast key: " << Utils::HexString((BYTE*)&podcastKey, 16) << std::endl;
|
||||
std::string podcastKeyStr = std::string(podcastKey, 16);
|
||||
|
||||
std::cout << "podcastKeyStr = " << podcastKeyStr << std::endl;
|
||||
|
||||
// Try decryption again with podcast key
|
||||
decAudioData = AttemptDecryption(audioData, podcastKeyStr);
|
||||
|
||||
if (decAudioData.compare(0, 5, "Error") == 0)
|
||||
return "Error: Could not properly decrypt podcast data (try downloading again)!";
|
||||
}
|
||||
else if (downloadStr.compare(0, 5, "Error") == 0)
|
||||
else
|
||||
return "Error: Could not properly decrypt song data (try downloading again)!";
|
||||
}
|
||||
|
||||
// Remove custom Spotify Ogg page from beginning of file and return audio data
|
||||
audioData = decAudioData.substr(audioData.find("\xFF\xFF\xFF\xFFOggS") + 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << downloadStr << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrypt encrypted song data with Tiny AES in C
|
||||
struct AES_ctx ctx;
|
||||
AES_init_ctx_iv(&ctx, reinterpret_cast<const uint8_t*>(&key[0]), IV);
|
||||
AES_CTR_xcrypt_buffer(&ctx, reinterpret_cast<uint8_t*>(&downloadStr[0]), downloadStr.size());
|
||||
|
||||
// Check if decrypted song data has valid header
|
||||
if (downloadStr.length() < 4 || downloadStr.compare(0, 4, "OggS") != 0)
|
||||
// Parse episode URL (fileId) to separate host and path
|
||||
try
|
||||
{
|
||||
std::cout << "Error: Could not properly decrypt song (try downloading again)!" << std::endl;
|
||||
std::regex re(urlRegex);
|
||||
std::smatch match;
|
||||
if (std::regex_search(fileId, match, re) && match.size() > 1)
|
||||
{
|
||||
songHost = match.str(1);
|
||||
songPath = match.str(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "Error: Download URL is not valid!";
|
||||
}
|
||||
}
|
||||
catch (std::regex_error& e)
|
||||
{
|
||||
// Syntax error in the regular expression
|
||||
return "Error: Invalid regex!";
|
||||
}
|
||||
|
||||
// Download episode data from URL
|
||||
audioData = Utils::DownloadSpotifyUrl(songHost, songPath, "");
|
||||
}
|
||||
|
||||
return audioData;
|
||||
}
|
||||
|
||||
static std::wstring songDir = songDirRoot;
|
||||
void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, std::string authToken, int quality)
|
||||
{
|
||||
std::string downloadStr;
|
||||
std::wstring songExtension = L".ogg";
|
||||
|
||||
SongInfo* songInfo = new SongInfo();
|
||||
|
||||
if (fileId.empty() || uri.length() < 13 || authToken.empty())
|
||||
{
|
||||
std::cout << "Could not download song or episode: missing fileId, trackUri, or authToken!" << std::endl;
|
||||
delete songInfo;
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove custom Spotify Ogg page from beginning of file
|
||||
downloadStr = downloadStr.substr(downloadStr.find("\xFF\xFF\xFF\xFFOggS") + 4);
|
||||
std::cout << "fileId = " + fileId << std::endl;
|
||||
std::cout << "uri = " + uri << std::endl;
|
||||
std::cout << "key = " + key << std::endl;
|
||||
std::cout << "keyHex = " + Utils::HexString((BYTE*)&key[0], 16) << std::endl;
|
||||
|
||||
if (uri.compare(0, 13, "spotify:track") == 0)
|
||||
{
|
||||
// Not an episode which means no predictable key
|
||||
if (memcmp(&key[0], podcastKey, 16) == 0 || memcmp(&key[0], nullKey, 16) == 0)
|
||||
{
|
||||
std::cout << "Error: Key is of the wrong type! Please try again." << std::endl;
|
||||
delete songInfo;
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "Downloading song..." << std::endl;
|
||||
|
||||
// Download song metadata from Spotify API
|
||||
std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/tracks/"
|
||||
@ -390,46 +457,10 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
||||
}
|
||||
else if (uri.length() > 15 && uri.compare(0, 15, "spotify:episode") == 0)
|
||||
{
|
||||
std::string episodeUrl = fileId;
|
||||
std::string songHost;
|
||||
std::string songPath;
|
||||
std::string songHost, songPath;
|
||||
|
||||
std::cout << "Downloading episode..." << std::endl;
|
||||
|
||||
// Parse episode URL to separate host and path
|
||||
try
|
||||
{
|
||||
std::regex re(urlRegex);
|
||||
std::smatch match;
|
||||
if (std::regex_search(episodeUrl, match, re) && match.size() > 1)
|
||||
{
|
||||
songHost = match.str(1);
|
||||
songPath = match.str(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: Download URL is not valid!" << std::endl;
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (std::regex_error& e)
|
||||
{
|
||||
// Syntax error in the regular expression
|
||||
std::cout << "Error: Invalid regex!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Download episode data from Spotify
|
||||
downloadStr = DownloadSpotifyUrl(songHost, songPath, "");
|
||||
|
||||
// TODO: MP3 files appear to have a variety of headers, not sure this is 100% reliable
|
||||
// Check if downloaded episode data has valid header
|
||||
/*if (downloadStr.length() < 3 || downloadStr.compare(0, 3, "ID3") != 0)
|
||||
{
|
||||
std::cout << "Error: Could not properly download podcast episode (try downloading again)!" << std::endl;
|
||||
return;
|
||||
}*/
|
||||
|
||||
// Download episode and show metadata from Spotify API
|
||||
std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/episodes/"
|
||||
+ uri.substr(uri.find("spotify:episode:") + 16), authToken);
|
||||
@ -438,8 +469,37 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
||||
songInfo->artist = strtok((char*)(metadata.substr(metadata.find("publisher\" :") + 14)).c_str(), "\"");
|
||||
songInfo->album = strtok((char*)(metadata.substr(metadata.find("media_type\" :") + 37)).c_str(), "\"");
|
||||
songInfo->coverUrl = strtok((char*)(metadata.substr(metadata.find("height\" :") + 28)).c_str(), "\"");
|
||||
songInfo->fileType = FileType::MP3;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Error: Invalid URI!" << std::endl;
|
||||
delete songInfo;
|
||||
return;
|
||||
}
|
||||
|
||||
downloadStr = DownloadAudioData(fileId, uri, key, authToken, quality);
|
||||
|
||||
if (downloadStr.length() < 6)
|
||||
{
|
||||
std::cout << "Error: Could not download audio!" << std::endl;
|
||||
delete songInfo;
|
||||
return;
|
||||
}
|
||||
else if (downloadStr.compare(0, 6, "<HTML>") == 0)
|
||||
{
|
||||
std::cout << "Error: " + downloadStr << std::endl;
|
||||
delete songInfo;
|
||||
return;
|
||||
}
|
||||
else if (downloadStr.compare(0, 5, "Error") == 0)
|
||||
{
|
||||
std::cout << downloadStr << std::endl;
|
||||
delete songInfo;
|
||||
return;
|
||||
}
|
||||
else if (downloadStr.compare(0, 3, "ID3") == 0)
|
||||
{
|
||||
songInfo->fileType = FileType::MP3;
|
||||
songExtension = L".mp3";
|
||||
}
|
||||
|
||||
@ -450,6 +510,7 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Separate filesystem logic into another function
|
||||
std::wstring tempDirArtist = FixPathStr(Utf8ToUtf16(songInfo->artist));
|
||||
|
||||
songDir = songDirRoot;
|
||||
@ -490,14 +551,12 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Couldn't create album directory!" << std::endl;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "Couldn't create artist directory!" << std::endl;
|
||||
}
|
||||
|
||||
delete songInfo;
|
||||
}
|
||||
|
||||
std::string Utils::DownloadSpotifyUrl(std::string host, std::string path, std::string authToken)
|
||||
|
Loading…
Reference in New Issue
Block a user