Reorganization of code to improve readability and also fix Spotify-hosted podcasts

This commit is contained in:
_ 2020-12-04 02:42:12 -07:00
parent a4e8e2a360
commit 576afb08f5

View File

@ -2,9 +2,15 @@
#include "Utils.h" #include "Utils.h"
#include "tiny-AES-c/aes.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 }; constexpr BYTE 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 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()@:%_\\+.~#?&\\/\\/=]*)"; "\\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) 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"; std::string AttemptDecryption(std::string data, std::string key)
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 downloadStr; // Decrypt encrypted data with Tiny AES in C
std::wstring songExtension = L".ogg"; 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(); // Check if decrypted song data has valid header (assume Ogg)
/*songInfo->title = ""; if (data.length() < 4 || data.compare(0, 4, "OggS") != 0)
songInfo->artist = ""; return "Error: Not a valid Ogg file after decryption! Please try again";
songInfo->album = "";
songInfo->coverUrl = "";
songInfo->fileType = FileType::OGG;*/
if (fileId.empty() || uri.empty() || authToken.empty()) return data;
{
std::cout << "Could not download song or episode: missing fileId, trackUri, or authToken!" << std::endl;
return;
} }
// Is length check even needed for compare()? std::string DownloadAudioData(std::string fileId, std::string uri, std::string key, std::string authToken, int quality)
if (uri.length() > 13 && uri.compare(0, 13, "spotify:track") == 0)
{ {
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!"; std::regex re(urlRegex);
return; 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 // Get storage resolve from Spotify
std::string urlNum = std::to_string(GetUrlNum(quality)); 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/files/audio/interactive_prefetch/" + fileId + "?product=0", authToken);
//"/storage-resolve/v2/files/audio/interactive/" + urlNum + "/" + fileId + "?product=0", authToken); //"/storage-resolve/v2/files/audio/interactive/" + urlNum + "/" + fileId + "?product=0", authToken);
if (srStr.length() <= 5) if (srStr.length() <= 5)
{ return "Error: Couldn't fetch storage resolve: (" + srStr + ")";
std::cout << "Error: Couldn't fetch storage resolve: (" + srStr + ")"<< std::endl; else if (srStr.substr(0, 5).compare("Error") == 0)
return; return srStr;
}
if (srStr.substr(0, 5).compare("Error") == 0)
{
std::cout << srStr << std::endl;
return;
}
// Parse storage resolve response to get the encrypted song data's URL // Parse storage resolve response to get the encrypted song data's URL
std::string songHost; std::string songHost;
@ -318,51 +331,105 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
} }
else else
{ {
std::cout << "Error: Download URL not found" << std::endl; return "Error: Download URL not found";
return;
} }
} }
catch (std::regex_error& e) catch (std::regex_error& e)
{ {
// Syntax error in the regular expression // Syntax error in the regular expression
std::cout << "Error: Invalid regex!" << std::endl; return "Error: Invalid regex!";
return;
} }
// Download encrypted song data from Spotify // Download encrypted audio data from Spotify
downloadStr = DownloadSpotifyUrl(songHost, songPath, ""); 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; // Only try again on podcasts
return; if (uri.length() > 15 && uri.compare(0, 15, "spotify:episode") == 0)
}
if (downloadStr.compare(0, 6, "<HTML>") == 0)
{ {
std::cout << "Error: " + downloadStr << std::endl; std::cout << "Podcast key: " << Utils::HexString((BYTE*)&podcastKey, 16) << std::endl;
return; 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; // Parse episode URL (fileId) to separate host and path
return; try
}
// 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)
{ {
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; return;
} }
// Remove custom Spotify Ogg page from beginning of file std::cout << "fileId = " + fileId << std::endl;
downloadStr = downloadStr.substr(downloadStr.find("\xFF\xFF\xFF\xFFOggS") + 4); 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 // Download song metadata from Spotify API
std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/tracks/" 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) else if (uri.length() > 15 && uri.compare(0, 15, "spotify:episode") == 0)
{ {
std::string episodeUrl = fileId; std::string songHost, songPath;
std::string songHost;
std::string songPath;
std::cout << "Downloading episode..." << std::endl; 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 // Download episode and show metadata from Spotify API
std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/episodes/" std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/episodes/"
+ uri.substr(uri.find("spotify:episode:") + 16), authToken); + 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->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->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->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"; songExtension = L".mp3";
} }
@ -450,6 +510,7 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
return; return;
} }
// TODO: Separate filesystem logic into another function
std::wstring tempDirArtist = FixPathStr(Utf8ToUtf16(songInfo->artist)); std::wstring tempDirArtist = FixPathStr(Utf8ToUtf16(songInfo->artist));
songDir = songDirRoot; songDir = songDirRoot;
@ -490,14 +551,12 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
return; return;
} }
else else
{
std::cout << "Couldn't create album directory!" << std::endl; std::cout << "Couldn't create album directory!" << std::endl;
} }
}
else else
{
std::cout << "Couldn't create artist directory!" << std::endl; std::cout << "Couldn't create artist directory!" << std::endl;
}
delete songInfo;
} }
std::string Utils::DownloadSpotifyUrl(std::string host, std::string path, std::string authToken) std::string Utils::DownloadSpotifyUrl(std::string host, std::string path, std::string authToken)