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 "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)
|
||||||
|
Loading…
Reference in New Issue
Block a user