Add support for podcasts; fix duplicate checker; add more error handling with downloading; added experimental code (disabled); README changes
This commit is contained in:
parent
e45143da82
commit
2ebf25b1b0
48
README.md
48
README.md
@ -1,34 +1,38 @@
|
|||||||
# SpotifyKeyDumper
|
# SpotifyKeyDumper
|
||||||
### By [@ProfessorTox](https://twitter.com/ProfessorTox)
|
### By [@ProfessorTox](https://twitter.com/ProfessorTox)
|
||||||
|
|
||||||
|
[![Discord](https://img.shields.io/discord/782069040713039882.svg?color=7389D8&label=SpotifyKeyDumper%20&logo=discord&logoColor=FFFFFF)](https://discord.gg/yPFQ9epABz)
|
||||||
|
|
||||||
Dump AES keys for Spotify songs from a compatible Windows Spotify version (compatibility listed below).
|
Dump AES keys for Spotify songs from a compatible Windows Spotify version (compatibility listed below).
|
||||||
|
|
||||||
Now with automatic download support (compatible versions listed below)!
|
**Now with automatic download support for songs and podcast episodes!**
|
||||||
|
|
||||||
* Note: *Currently not compatible with premium accounts*
|
|
||||||
|
|
||||||
![Screenshot Example](./screenshot_example.png)
|
|
||||||
|
|
||||||
## Compatibility
|
|
||||||
Crossed out items support key dumping but not automatic downloading
|
|
||||||
* ~~1.1.25~~
|
|
||||||
* ~~1.1.26~~
|
|
||||||
* ~~1.1.27~~
|
|
||||||
* ~~1.1.28~~
|
|
||||||
* ~~1.1.29~~
|
|
||||||
* ~~1.1.30~~
|
|
||||||
* ~~1.1.44~~
|
|
||||||
* 1.1.45
|
|
||||||
* 1.1.46
|
|
||||||
* 1.1.47
|
|
||||||
|
|
||||||
## Using
|
## Using
|
||||||
1. Make sure `SpotifyKeyDumperInjector.exe` and `SpotifyKeyDumper.dll` are located in the same folder as Spotify (`Spotify.exe`).
|
1. Go to `%appdata%\Spotify` (or wherever your Spotify installation is located)
|
||||||
2. Start SpotifyKeyDumperInjector before launching Spotify.
|
2. Make sure `SpotifyKeyDumperInjector.exe` and `SpotifyKeyDumper.dll` are located in the same place as `Spotify.exe`.
|
||||||
|
3. Start SpotifyKeyDumperInjector (requires administrator) before launching Spotify.
|
||||||
|
4. Each song or podcast episode (after the first one) that plays will be automatically downloaded and placed
|
||||||
|
under `%appdata%\Spotify\Downloads`.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
* *Crossed out items support key dumping but not automatic downloading.*
|
||||||
|
|
||||||
|
Spotify version:
|
||||||
|
* 1.1.47
|
||||||
|
* 1.1.46
|
||||||
|
* 1.1.45
|
||||||
|
* ~~1.1.44~~
|
||||||
|
* ~~1.1.30~~
|
||||||
|
* ~~1.1.29~~
|
||||||
|
* ~~1.1.28~~
|
||||||
|
* ~~1.1.27~~
|
||||||
|
* ~~1.1.26~~
|
||||||
|
* ~~1.1.25~~
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
This project uses C++14 on Visual Studio 2019
|
This project uses C++14 on Visual Studio 2019.
|
||||||
|
|
||||||
If you want a specific version, DM me.
|
*If you want a specific version, create an issue.*
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
* Inspired by XSpotify
|
* Inspired by XSpotify
|
||||||
|
@ -27,10 +27,20 @@ signalEmitter_v45 signalEmitter_v45_hook = nullptr;
|
|||||||
typedef void (__cdecl* keyDecoder_v47)();
|
typedef void (__cdecl* keyDecoder_v47)();
|
||||||
keyDecoder_v47 keyDecoder_v47_hook = nullptr;
|
keyDecoder_v47 keyDecoder_v47_hook = nullptr;
|
||||||
|
|
||||||
|
typedef int (__thiscall* startUpdate_v47)(char* This, char* newVerChars, char** updateUrlPtr, void* a4, int a5, void* a6,
|
||||||
|
int a7);
|
||||||
|
startUpdate_v47 startUpdate_v47_hook = nullptr;
|
||||||
|
|
||||||
|
typedef void (__thiscall* uriHandler_v47)(DWORD* This, void* a2, int a3, char a4);
|
||||||
|
uriHandler_v47 uriHandler_v47_hook = nullptr;
|
||||||
|
|
||||||
|
typedef bool (__cdecl* isValidUrl_v25)(int* urlThingArg, int a2);
|
||||||
|
isValidUrl_v25 isValidUrl_v25_hook = nullptr;
|
||||||
|
|
||||||
std::string authToken = std::string();
|
std::string authToken = std::string();
|
||||||
std::string keyStr = std::string();
|
std::string keyStr = std::string();
|
||||||
|
|
||||||
std::string trackUriStr = std::string();
|
std::string uriStr = std::string();
|
||||||
__int64 newPosition = 0;
|
__int64 newPosition = 0;
|
||||||
bool signalled = false;
|
bool signalled = false;
|
||||||
int destKeyPtr = 0;
|
int destKeyPtr = 0;
|
||||||
@ -124,7 +134,7 @@ int* __fastcall authToken_hook_v45(void* This, void* _EDX, int* a2)
|
|||||||
int* __fastcall openTrack_hook_v45(void* This, void* _EDX, int a2, void* a3, int a4, __int64 position, char a6,
|
int* __fastcall openTrack_hook_v45(void* This, void* _EDX, int a2, void* a3, int a4, __int64 position, char a6,
|
||||||
void* a7)
|
void* a7)
|
||||||
{
|
{
|
||||||
std::cout << "openTrack!!!" << std::endl << std::endl;
|
//std::cout << "openTrack!!!" << std::endl << std::endl;
|
||||||
|
|
||||||
return openTrack_v45_hook(This, a2, a3, a4, newPosition, a6, a7);
|
return openTrack_v45_hook(This, a2, a3, a4, newPosition, a6, a7);
|
||||||
}
|
}
|
||||||
@ -138,20 +148,23 @@ int* __fastcall log_hook_v45(void* This, void* _EDX, int a2, int a3, void* a4, c
|
|||||||
|
|
||||||
if (!Utils::BadPtr(logChars))
|
if (!Utils::BadPtr(logChars))
|
||||||
{
|
{
|
||||||
//std::string logStr = std::string(logChars).substr(8, 5);
|
|
||||||
std::string logStr = std::string(logChars);
|
std::string logStr = std::string(logChars);
|
||||||
|
|
||||||
//std::cout << "logStr: " << logStr << std::endl;
|
|
||||||
|
|
||||||
if (logStr.length() > 32 && logStr.compare(8, 9, "track_uri") == 0) // 19 + 13 = 32
|
if (logStr.length() > 32 && logStr.compare(8, 9, "track_uri") == 0) // 19 + 13 = 32
|
||||||
{
|
{
|
||||||
if (logStr.compare(19, 13, "spotify:track") == 0)
|
if (logStr.compare(19, 13, "spotify:track") == 0)
|
||||||
{
|
{
|
||||||
//std::cout << "Track URI: " << logStr.substr(19, std::string::npos) << std::endl;
|
//std::cout << "Track URI: " << logStr.substr(19, std::string::npos) << std::endl;
|
||||||
trackUriStr = logStr.substr(19, std::string::npos);
|
uriStr = logStr.substr(19, std::string::npos);
|
||||||
newPosition = 0;
|
newPosition = 0;
|
||||||
}
|
}
|
||||||
// TODO
|
else if (logStr.length() > 34 && logStr.compare(19, 15, "spotify:episode") == 0)
|
||||||
|
{
|
||||||
|
//std::cout << "Episode URI: " << logStr.substr(19, std::string::npos) << std::endl;
|
||||||
|
uriStr = logStr.substr(19, std::string::npos);
|
||||||
|
newPosition = 0;
|
||||||
|
}
|
||||||
|
// TODO: e.g. spotify:ad:000000014990f3ec000000203522e473
|
||||||
//else if (logStr.compare(19, 11, "spotify:ad") == 0) // Possibly this works?
|
//else if (logStr.compare(19, 11, "spotify:ad") == 0) // Possibly this works?
|
||||||
/*else
|
/*else
|
||||||
{
|
{
|
||||||
@ -165,20 +178,18 @@ int* __fastcall log_hook_v45(void* This, void* _EDX, int a2, int a3, void* a4, c
|
|||||||
return log_v45_hook(This, a2, a3, a4, classStr, a6, logThing);
|
return log_v45_hook(This, a2, a3, a4, classStr, a6, logThing);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string lastKey = std::string();
|
std::string lastUri = std::string();
|
||||||
void __fastcall fileIdWriter_hook_v45(void* This, void* _EDX, int* a2)
|
void __fastcall fileIdWriter_hook_v45(void* This, void* _EDX, int* a2)
|
||||||
{
|
{
|
||||||
// [[ebp+8]+28]
|
// [[ebp+8]+28]
|
||||||
char* fileId = (char*) *(DWORD*)(a2 + 16); // 0x40 / 4 = 16
|
char* fileId = (char*) *(DWORD*)(a2 + 16); // 0x40 / 4 = 16
|
||||||
|
|
||||||
//std::cout << "fileId: " << fileId << std::endl << std::endl;
|
if (signalled && lastUri.compare(uriStr) != 0)
|
||||||
|
|
||||||
if (signalled && lastKey.compare(keyStr) != 0)
|
|
||||||
{
|
{
|
||||||
//std::cout << "signalled = false" << std::endl;
|
//std::cout << "signalled = false" << std::endl;
|
||||||
signalled = false;
|
signalled = false;
|
||||||
lastKey = keyStr;
|
lastUri = uriStr;
|
||||||
std::thread t2(Utils::DownloadSong, std::string(fileId), trackUriStr, keyStr, authToken);
|
std::thread t2(Utils::DownloadSong, std::string(fileId), uriStr, keyStr, authToken);
|
||||||
t2.detach();
|
t2.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,6 +271,53 @@ __declspec(naked) void keyDecoder_hook_v47()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int __fastcall startUpdate_hook_v47(char* This, void* _EDX, char* newVerChars, char** updateUrlPtr, void* a4, int a5,
|
||||||
|
void* a6, int a7)
|
||||||
|
{
|
||||||
|
*updateUrlPtr[0] = '\x00'; // Breaks the URL to prevent updating
|
||||||
|
|
||||||
|
return startUpdate_v47_hook(This, newVerChars, updateUrlPtr, a4, a5, a6, a7);
|
||||||
|
}
|
||||||
|
|
||||||
|
void __fastcall uriHandler_hook_v47(DWORD* This, void* _EDX, void* a2, int a3, char a4)
|
||||||
|
{
|
||||||
|
int uriType = *This;
|
||||||
|
|
||||||
|
// Check if uri type is an ad (12)
|
||||||
|
if (uriType == 12)
|
||||||
|
{
|
||||||
|
DWORD* adUri = This - 42; // 168 / 4 = 42
|
||||||
|
char* adUriChars = (char*)*adUri;
|
||||||
|
|
||||||
|
for (int index = 0; index < 32; index++)
|
||||||
|
adUriChars[index] = '0';
|
||||||
|
|
||||||
|
//std::cout << "Ad uri: spotify:ad:" << std::string((char*)*adUri, 32) << std::endl;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uriHandler_v47_hook(This, a2, a3, a4);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool __cdecl isValidUrl_hook_v25(int* urlThingArg, int arg_4)
|
||||||
|
{
|
||||||
|
unsigned char* source = (unsigned char*)*(void**)*(urlThingArg + 1);
|
||||||
|
std::u16string u16_str(reinterpret_cast<const char16_t*>(source));
|
||||||
|
std::string utf8Url = std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>{}.to_bytes(u16_str);
|
||||||
|
|
||||||
|
//std::cout << "URL: " << utf8Url << std::endl;
|
||||||
|
|
||||||
|
if (utf8Url.find("doubleclick") != std::string::npos)
|
||||||
|
{
|
||||||
|
//printf("Blocked URL: %s\n", utf8Url.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return isValidUrl_v25_hook(urlThingArg, arg_4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
char* GetKeyFuncAddrV26()
|
char* GetKeyFuncAddrV26()
|
||||||
{
|
{
|
||||||
BYTE ref_v19 = 0x55;
|
BYTE ref_v19 = 0x55;
|
||||||
@ -330,9 +388,9 @@ void Hooks::Init()
|
|||||||
case 45:
|
case 45:
|
||||||
keyToLE_v28_hook = (keyToLE_v28)Utils::TrampHook32((char*)0x010CF780, (char*)keyToLE_hook_v28, 6);
|
keyToLE_v28_hook = (keyToLE_v28)Utils::TrampHook32((char*)0x010CF780, (char*)keyToLE_hook_v28, 6);
|
||||||
authToken_v45_hook = (authToken_v45)Utils::TrampHook32((char*)0x00BF75F0, (char*)authToken_hook_v45, 7);
|
authToken_v45_hook = (authToken_v45)Utils::TrampHook32((char*)0x00BF75F0, (char*)authToken_hook_v45, 7);
|
||||||
//openTrack_v45_hook = (openTrack_v45)Utils::TrampHook32((char*)0x00CA5740, (char*)&openTrack_hook_v45, 5);
|
//openTrack_v45_hook = (openTrack_v45)Utils::TrampHook32((char*)0x00CA5740, (char*)openTrack_hook_v45, 5);
|
||||||
log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010F2370, (char*)&log_hook_v45, 5);
|
log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010F2370, (char*)log_hook_v45, 5);
|
||||||
fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CBB560, (char*)&fileIdWriter_hook_v45,
|
fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CBB560, (char*)fileIdWriter_hook_v45,
|
||||||
5);
|
5);
|
||||||
signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00B095A0, (char*)signalEmitter_hook_v45,
|
signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00B095A0, (char*)signalEmitter_hook_v45,
|
||||||
5);
|
5);
|
||||||
@ -340,8 +398,8 @@ void Hooks::Init()
|
|||||||
case 46:
|
case 46:
|
||||||
keyToLE_v28_hook = (keyToLE_v28)Utils::TrampHook32((char*)0x010C2FB0, (char*)keyToLE_hook_v28, 6);
|
keyToLE_v28_hook = (keyToLE_v28)Utils::TrampHook32((char*)0x010C2FB0, (char*)keyToLE_hook_v28, 6);
|
||||||
authToken_v45_hook = (authToken_v45)Utils::TrampHook32((char*)0x00BEC8E0, (char*)authToken_hook_v45, 7);
|
authToken_v45_hook = (authToken_v45)Utils::TrampHook32((char*)0x00BEC8E0, (char*)authToken_hook_v45, 7);
|
||||||
log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010E59E0, (char*)&log_hook_v45, 5);
|
log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010E59E0, (char*)log_hook_v45, 5);
|
||||||
fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CB00D0, (char*)&fileIdWriter_hook_v45,
|
fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CB00D0, (char*)fileIdWriter_hook_v45,
|
||||||
5);
|
5);
|
||||||
signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00B02270, (char*)signalEmitter_hook_v45,
|
signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00B02270, (char*)signalEmitter_hook_v45,
|
||||||
5);
|
5);
|
||||||
@ -349,13 +407,16 @@ void Hooks::Init()
|
|||||||
case 47:
|
case 47:
|
||||||
keyBuffer_v47 = new char[16]; // 128 bits = 16 bytes
|
keyBuffer_v47 = new char[16]; // 128 bits = 16 bytes
|
||||||
keyToLE_v28_hook = (keyToLE_v28)Utils::TrampHook32((char*)0x010C5B00, (char*)keyToLE_hook_v47, 6);
|
keyToLE_v28_hook = (keyToLE_v28)Utils::TrampHook32((char*)0x010C5B00, (char*)keyToLE_hook_v47, 6);
|
||||||
keyDecoder_v47_hook = (keyDecoder_v47)Utils::TrampHook32((char*)0x0153148D /*0x015337CD*/, (char*)keyDecoder_hook_v47, 7 /*6*/);
|
keyDecoder_v47_hook = (keyDecoder_v47)Utils::TrampHook32((char*)0x0153148D, (char*)keyDecoder_hook_v47, 7);
|
||||||
authToken_v45_hook = (authToken_v45)Utils::TrampHook32((char*)0x00BED0F0, (char*)authToken_hook_v45, 7);
|
authToken_v45_hook = (authToken_v45)Utils::TrampHook32((char*)0x00BED0F0, (char*)authToken_hook_v45, 7);
|
||||||
log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010E8750, (char*)&log_hook_v45, 5);
|
log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010E8750, (char*)log_hook_v45, 5);
|
||||||
fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CB0630, (char*)&fileIdWriter_hook_v45,
|
fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CB0630, (char*)fileIdWriter_hook_v45,
|
||||||
5);
|
5);
|
||||||
signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00AFBB50, (char*)signalEmitter_hook_v45,
|
signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00AFBB50, (char*)signalEmitter_hook_v45,
|
||||||
5);
|
5);
|
||||||
|
//startUpdate_v47_hook = (startUpdate_v47)Utils::TrampHook32((char*)0x009FB530, (char*)startUpdate_hook_v47, 5);
|
||||||
|
//uriHandler_v47_hook = (uriHandler_v47)Utils::TrampHook32((char*)0x010A39E0, (char*)uriHandler_hook_v47, 5);
|
||||||
|
//isValidUrl_v25_hook = (isValidUrl_v25)Utils::TrampHook32((char*)0x0105CD80, (char*)isValidUrl_hook_v25, 6);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -120,134 +120,210 @@ struct SongInfo
|
|||||||
std::string title, artist, album, cover;
|
std::string title, artist, album, cover;
|
||||||
} songInfo;
|
} songInfo;
|
||||||
|
|
||||||
|
void ClearSongInfo()
|
||||||
|
{
|
||||||
|
songInfo.title = "";
|
||||||
|
songInfo.artist = "";
|
||||||
|
songInfo.album = "";
|
||||||
|
songInfo.cover = "";
|
||||||
|
}
|
||||||
|
|
||||||
static const std::string songRegex =
|
static const std::string songRegex =
|
||||||
"https?:\\/\\/(?:www\.)?([-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})"
|
"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::string albumSearchPattern = "\x68\x65\x69\x67\x68\x74\x22\x20\x3A\x20\x36\x34\x30";
|
||||||
static const std::wstring songDirRoot = L"Downloads";
|
static const std::wstring songDirRoot = L"Downloads";
|
||||||
static std::wstring songDir = songDirRoot;
|
static std::wstring songDir = songDirRoot;
|
||||||
void Utils::DownloadSong(std::string fileId, std::string trackUri, std::string key, std::string authToken)
|
void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, std::string authToken)
|
||||||
{
|
{
|
||||||
std::cout << "Downloading song..." << std::endl;
|
std::string downloadStr;
|
||||||
|
std::wstring songExtension = L".ogg";
|
||||||
|
|
||||||
if (fileId.empty() || trackUri.empty() || key.empty() || authToken.empty())
|
if (fileId.empty() || uri.empty() || authToken.empty())
|
||||||
{
|
{
|
||||||
std::cout << "Could not download song: missing fileId, trackUri, key, or authToken!" << std::endl;
|
std::cout << "Could not download song or episode: missing fileId, trackUri, or authToken!" << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get storage resolve from Spotify
|
// Is length check even needed for compare()?
|
||||||
std::string srStr = DownloadSpotifyUrl("spclient.wg.spotify.com",
|
if (uri.length() > 13 && uri.compare(0, 13, "spotify:track") == 0)
|
||||||
"/storage-resolve/files/audio/interactive_prefetch/" + fileId + "?product=0", authToken);
|
|
||||||
|
|
||||||
if (srStr.length() <= 5)
|
|
||||||
{
|
{
|
||||||
std::cout << "Error: Couldn't fetch storage resolve!" << std::endl;
|
std::cout << "Downloading song..." << std::endl;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (srStr.substr(0, 5).compare("Error") == 0)
|
if (key.empty())
|
||||||
{
|
|
||||||
std::cout << srStr << std::endl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse storage resolve response to get the encrypted song data's URL
|
|
||||||
std::string songHost;
|
|
||||||
std::string songPath;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
std::regex re(songRegex);
|
|
||||||
std::smatch match;
|
|
||||||
if (std::regex_search(srStr, match, re) && match.size() > 1)
|
|
||||||
{
|
{
|
||||||
songHost = match.str(1);
|
std::cout << "Could not download song: missing key!";
|
||||||
songPath = match.str(2);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
std::cout << "Error: Download URL not found" << std::endl;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (std::regex_error& e)
|
|
||||||
{
|
|
||||||
// Syntax error in the regular expression
|
|
||||||
std::cout << "Error: regex_error" << std::endl;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download encrypted song data from Spotify
|
// Get storage resolve from Spotify
|
||||||
std::string songStr = DownloadSpotifyUrl(songHost, songPath, "");
|
std::string srStr = DownloadSpotifyUrl("spclient.wg.spotify.com",
|
||||||
|
"/storage-resolve/files/audio/interactive_prefetch/" + fileId + "?product=0", authToken);
|
||||||
|
|
||||||
if (songStr.length() <= 6)
|
if (srStr.length() <= 5)
|
||||||
{
|
{
|
||||||
std::cout << "Error: Could not download audio!" << std::endl;
|
std::cout << "Error: Couldn't fetch storage resolve!" << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (songStr.substr(0, 6).compare("<HTML>") == 0)
|
if (srStr.substr(0, 5).compare("Error") == 0)
|
||||||
{
|
{
|
||||||
std::cout << "Error: " + songStr << std::endl;
|
std::cout << srStr << std::endl;
|
||||||
return;
|
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*>(&songStr[0]), songStr.size());
|
|
||||||
|
|
||||||
// Remove custom Spotify Ogg page from beginning of file
|
// Parse storage resolve response to get the encrypted song data's URL
|
||||||
songStr = songStr.substr(songStr.find("\xFF\xFF\xFF\xFFOggS") + 4);
|
std::string songHost;
|
||||||
|
std::string songPath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::regex re(songRegex);
|
||||||
|
std::smatch match;
|
||||||
|
if (std::regex_search(srStr, match, re) && match.size() > 1)
|
||||||
|
{
|
||||||
|
songHost = match.str(1);
|
||||||
|
songPath = match.str(2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cout << "Error: Download URL not found" << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (std::regex_error& e)
|
||||||
|
{
|
||||||
|
// Syntax error in the regular expression
|
||||||
|
std::cout << "Error: Invalid regex!" << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!trackUri.empty())
|
// Download encrypted song data from Spotify
|
||||||
{
|
downloadStr = DownloadSpotifyUrl(songHost, songPath, "");
|
||||||
|
|
||||||
|
if (downloadStr.length() <= 6)
|
||||||
|
{
|
||||||
|
std::cout << "Error: Could not download audio!" << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadStr.substr(0, 6).compare("<HTML>") == 0)
|
||||||
|
{
|
||||||
|
std::cout << "Error: " + 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());
|
||||||
|
|
||||||
|
// Remove custom Spotify Ogg page from beginning of file
|
||||||
|
downloadStr = downloadStr.substr(downloadStr.find("\xFF\xFF\xFF\xFFOggS") + 4);
|
||||||
|
|
||||||
|
// Download song metadata from Spotify API
|
||||||
std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/tracks/"
|
std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/tracks/"
|
||||||
+ trackUri.substr(trackUri.find("spotify:track:") + 14), authToken);
|
+ uri.substr(uri.find("spotify:track:") + 14), authToken);
|
||||||
|
|
||||||
songInfo.title = strtok((char*)(metadata.substr(metadata.find("is_local") + 55)).c_str(), "\"");
|
songInfo.title = strtok((char*)(metadata.substr(metadata.find("is_local") + 55)).c_str(), "\"");
|
||||||
songInfo.artist = strtok((char*)(metadata.substr(metadata.find("name") + 9)).c_str(), "\"");
|
songInfo.artist = strtok((char*)(metadata.substr(metadata.find("name") + 9)).c_str(), "\"");
|
||||||
songInfo.album = strtok((char*)(metadata.substr(metadata.find(albumSearchPattern) + 404)).c_str(), "\"");
|
songInfo.album = strtok((char*)(metadata.substr(metadata.find(albumSearchPattern) + 404)).c_str(), "\"");
|
||||||
songInfo.cover = strtok((char*)(metadata.substr(metadata.find("height") + 30)).c_str(), "\"");
|
songInfo.cover = strtok((char*)(metadata.substr(metadata.find("height") + 30)).c_str(), "\"");
|
||||||
|
|
||||||
std::wstring tempDirArtist = FixPathStr(Utf8ToUtf16(songInfo.artist));
|
songExtension = L".ogg";
|
||||||
|
}
|
||||||
|
else if (uri.length() > 15 && uri.compare(0, 15, "spotify:episode") == 0)
|
||||||
|
{
|
||||||
|
std::string episodeUrl = fileId;
|
||||||
|
std::string songHost;
|
||||||
|
std::string songPath;
|
||||||
|
|
||||||
songDir = songDirRoot;
|
std::cout << "Downloading episode..." << std::endl;
|
||||||
if (!CreateDirectoryW(songDir.c_str(), NULL) && ERROR_ALREADY_EXISTS != GetLastError())
|
|
||||||
std::cout << "Couldn't create main downloads directory!" << std::endl;
|
|
||||||
|
|
||||||
if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist).c_str(), NULL)
|
// Parse episode URL to separate host and path
|
||||||
|| ERROR_ALREADY_EXISTS == GetLastError())
|
try
|
||||||
{
|
{
|
||||||
std::wstring tempDirAlbum = FixPathStr(Utf8ToUtf16(songInfo.album));
|
std::regex re(songRegex);
|
||||||
|
std::smatch match;
|
||||||
if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist + std::wstring(L"\\")
|
if (std::regex_search(episodeUrl, match, re) && match.size() > 1)
|
||||||
+ tempDirAlbum).c_str(), NULL) || ERROR_ALREADY_EXISTS == GetLastError())
|
|
||||||
{
|
{
|
||||||
songDir += L"\\" + tempDirArtist + std::wstring(L"\\") + tempDirAlbum;
|
songHost = match.str(1);
|
||||||
|
songPath = match.str(2);
|
||||||
std::wstring tempDirSong = FixPathStr(Utf8ToUtf16(songInfo.title));
|
|
||||||
|
|
||||||
std::ofstream songFileOut(songDir + L".\\" + tempDirArtist + L" - " + tempDirSong + L".ogg",
|
|
||||||
std::ios_base::binary);
|
|
||||||
songFileOut.write(songStr.c_str(), songStr.size());
|
|
||||||
songFileOut.close();
|
|
||||||
|
|
||||||
std::cout << "Finished downloading: " << songInfo.artist << " - \"" << songInfo.title << "\"!" << std::endl;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
std::cout << "Couldn't create album directory!" << std::endl;
|
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 encrypted song data from Spotify
|
||||||
|
downloadStr = DownloadSpotifyUrl(songHost, songPath, "");
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
songInfo.title = strtok((char*)(metadata.substr(metadata.find("name") + 9)).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.cover = strtok((char*)(metadata.substr(metadata.find("height") + 28)).c_str(), "\"");
|
||||||
|
|
||||||
|
songExtension = L".mp3";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songInfo.title.empty() || songInfo.artist.empty() || songInfo.album.empty())
|
||||||
|
{
|
||||||
|
std::cout << "Error: Invalid title/artist/album name!" << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring tempDirArtist = FixPathStr(Utf8ToUtf16(songInfo.artist));
|
||||||
|
|
||||||
|
songDir = songDirRoot;
|
||||||
|
if (!CreateDirectoryW(songDir.c_str(), NULL) && ERROR_ALREADY_EXISTS != GetLastError())
|
||||||
|
std::cout << "Couldn't create main downloads directory!" << std::endl;
|
||||||
|
|
||||||
|
if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist).c_str(), NULL)
|
||||||
|
|| ERROR_ALREADY_EXISTS == GetLastError())
|
||||||
|
{
|
||||||
|
std::wstring tempDirAlbum = FixPathStr(Utf8ToUtf16(songInfo.album));
|
||||||
|
|
||||||
|
if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist + std::wstring(L"\\")
|
||||||
|
+ tempDirAlbum).c_str(), NULL) || ERROR_ALREADY_EXISTS == GetLastError())
|
||||||
|
{
|
||||||
|
songDir += L"\\" + tempDirArtist + std::wstring(L"\\") + tempDirAlbum;
|
||||||
|
|
||||||
|
std::wstring tempDirSong = FixPathStr(Utf8ToUtf16(songInfo.title));
|
||||||
|
|
||||||
|
std::ofstream songFileOut(songDir + L".\\" + tempDirArtist + L" - " + tempDirSong + songExtension,
|
||||||
|
std::ios_base::binary);
|
||||||
|
songFileOut.write(downloadStr.c_str(), downloadStr.size());
|
||||||
|
songFileOut.close();
|
||||||
|
|
||||||
|
std::cout << "Finished downloading: " << songInfo.artist << " - \"" << songInfo.title << "\"!" << std::endl;
|
||||||
|
|
||||||
|
ClearSongInfo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
std::cout << "Couldn't create artist directory!" << std::endl;
|
std::cout << "Couldn't create album directory!" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Could not finish downloading song!" << std::endl;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cout << "Couldn't create artist directory!" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Could not finish downloading song!" << std::endl;
|
||||||
|
|
||||||
|
ClearSongInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GetLastErrorAsString()
|
std::string GetLastErrorAsString()
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
#pragma comment(lib, "Version.lib")
|
#pragma comment(lib, "Version.lib")
|
||||||
#pragma comment(lib, "Wininet.lib")
|
#pragma comment(lib, "Wininet.lib")
|
||||||
|
|
||||||
|
#include <codecvt>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include "framework.h"
|
#include "framework.h"
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 842 KiB |
Loading…
Reference in New Issue
Block a user