Add tagging support & warn on unsupported bitrate
This commit is contained in:
parent
4cd1139b8b
commit
aa323ee11b
@ -34,7 +34,13 @@ Spotify version:
|
|||||||
* ~~1.1.25~~
|
* ~~1.1.25~~
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
This project uses C++14 on Visual Studio 2019.
|
This project uses C++14 on Visual Studio 2019 with vcpkg:
|
||||||
|
1. [Install vcpkg](https://github.com/microsoft/vcpkg#quick-start-windows) if you haven't done so already
|
||||||
|
2. Install TagLib dependency with vcpkg (`vcpkg install taglib:x86-windows-static-md`)
|
||||||
|
3. Clone the repository (`git clone https://gitlab.com/fuck-capitalism/spotifykeydumper.git`)
|
||||||
|
4. Open the solution 'SpotifyKeyDumper.sln' in Visual Studio 2019 and set the configuration to Release and the
|
||||||
|
platform to x86 and then select Build -> Build Solution **or** run MSBuild (requires Visual Studio 2019 installation):
|
||||||
|
`msbuild /p:Configuration="Release" /p:Platform="x86"`.
|
||||||
|
|
||||||
*If you want a specific version, create an issue.*
|
*If you want a specific version, create an issue.*
|
||||||
|
|
||||||
|
@ -37,10 +37,14 @@ uriHandler_v47 uriHandler_v47_hook = nullptr;
|
|||||||
typedef bool (__cdecl* isValidUrl_v25)(int* urlThingArg, int a2);
|
typedef bool (__cdecl* isValidUrl_v25)(int* urlThingArg, int a2);
|
||||||
isValidUrl_v25 isValidUrl_v25_hook = nullptr;
|
isValidUrl_v25 isValidUrl_v25_hook = nullptr;
|
||||||
|
|
||||||
|
typedef int (__cdecl* getBitrate_v25)(int quality);
|
||||||
|
getBitrate_v25 getBitrate_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 uriStr = std::string();
|
std::string uriStr = std::string();
|
||||||
std::string fileId = std::string();
|
std::string fileId = std::string();
|
||||||
|
int quality = 4;
|
||||||
|
|
||||||
__int64 newPosition = 0;
|
__int64 newPosition = 0;
|
||||||
bool signalled = false;
|
bool signalled = false;
|
||||||
@ -54,7 +58,7 @@ void TryDownload()
|
|||||||
{
|
{
|
||||||
signalled = false;
|
signalled = false;
|
||||||
lastUri = uriStr;
|
lastUri = uriStr;
|
||||||
std::thread t2(Utils::DownloadSong, std::string(fileId), uriStr, keyStr, authToken);
|
std::thread t2(Utils::DownloadSong, std::string(fileId), uriStr, keyStr, authToken, quality);
|
||||||
t2.detach();
|
t2.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,7 +67,7 @@ int __cdecl keyToLE_hook_v25(unsigned int* dest, int* key, int bits)
|
|||||||
{
|
{
|
||||||
if (bits == 128)
|
if (bits == 128)
|
||||||
{
|
{
|
||||||
BYTE keyBuffer[16];
|
BYTE keyBuffer[16] = {};
|
||||||
BYTE* keyBufPtr = keyBuffer;
|
BYTE* keyBufPtr = keyBuffer;
|
||||||
memcpy(keyBufPtr, key, 16);
|
memcpy(keyBufPtr, key, 16);
|
||||||
|
|
||||||
@ -88,7 +92,7 @@ int __cdecl keyToLE_hook_v28(unsigned int* dest, int* key, int bits, bool isEnco
|
|||||||
{
|
{
|
||||||
// key is encoded with some sort of algorithm; decode it here
|
// key is encoded with some sort of algorithm; decode it here
|
||||||
|
|
||||||
unsigned int keyDecoded[4];
|
unsigned int keyDecoded[4] = {};
|
||||||
unsigned int uVar1;
|
unsigned int uVar1;
|
||||||
unsigned int keyPtr;
|
unsigned int keyPtr;
|
||||||
unsigned int uVar3;
|
unsigned int uVar3;
|
||||||
@ -118,7 +122,7 @@ int __cdecl keyToLE_hook_v28(unsigned int* dest, int* key, int bits, bool isEnco
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy key bytes to new buffer
|
// Copy key bytes to new buffer
|
||||||
char keyBuffer[16];
|
char keyBuffer[16] = {};
|
||||||
char* keyBufPtr = keyBuffer;
|
char* keyBufPtr = keyBuffer;
|
||||||
memcpy(keyBufPtr, decodedKeyPtr, 16);
|
memcpy(keyBufPtr, decodedKeyPtr, 16);
|
||||||
|
|
||||||
@ -331,6 +335,57 @@ bool __cdecl isValidUrl_hook_v25(int* urlThingArg, int arg_4)
|
|||||||
return isValidUrl_v25_hook(urlThingArg, arg_4);
|
return isValidUrl_v25_hook(urlThingArg, arg_4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Fix downloads on low quality and very high quality to get rid of this
|
||||||
|
bool hasAlertedQuality = false;
|
||||||
|
int __cdecl getBitrate_hook_v25(int quality)
|
||||||
|
{
|
||||||
|
// Force quality to be normal or high quality (since other modes do not reliably work)
|
||||||
|
// (no, forcing it to very high on a free account does not work)
|
||||||
|
|
||||||
|
/*switch (quality)
|
||||||
|
{
|
||||||
|
case 0: // Automatic
|
||||||
|
quality = 3;
|
||||||
|
break;
|
||||||
|
case 1: // Low
|
||||||
|
quality = 2;
|
||||||
|
break;
|
||||||
|
case 4: // Very high
|
||||||
|
quality = 3;
|
||||||
|
break;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
std::string qualityStr = "?";
|
||||||
|
switch (quality)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
qualityStr = "Automatic";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
qualityStr = "Low";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
qualityStr = "Normal";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
qualityStr = "High";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
qualityStr = "Very High";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAlertedQuality && quality != 2 && quality != 3)
|
||||||
|
{
|
||||||
|
std::cout << "Warning! You have selected an unsupported quality (" + qualityStr + "); select Normal or High"
|
||||||
|
" quality in the Spotify settings for proper compatibility." << std::endl;
|
||||||
|
hasAlertedQuality = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
::quality = quality;
|
||||||
|
|
||||||
|
return getBitrate_v25_hook(quality);
|
||||||
|
}
|
||||||
|
|
||||||
char* GetKeyFuncAddrV26()
|
char* GetKeyFuncAddrV26()
|
||||||
{
|
{
|
||||||
@ -407,7 +462,8 @@ void Hooks::Init()
|
|||||||
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); // 00E48410
|
||||||
|
getBitrate_v25_hook = (getBitrate_v25)Utils::TrampHook32((char*)0x00E48410, (char*)getBitrate_hook_v25, 6);
|
||||||
break;
|
break;
|
||||||
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);
|
||||||
@ -417,6 +473,7 @@ void Hooks::Init()
|
|||||||
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);
|
||||||
|
getBitrate_v25_hook = (getBitrate_v25)Utils::TrampHook32((char*)0x00E3C780, (char*)getBitrate_hook_v25, 6);
|
||||||
break;
|
break;
|
||||||
case 47:
|
case 47:
|
||||||
keyBuffer_v47 = new char[16]; // 128 bits = 16 bytes
|
keyBuffer_v47 = new char[16]; // 128 bits = 16 bytes
|
||||||
@ -428,6 +485,7 @@ void Hooks::Init()
|
|||||||
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);
|
||||||
|
getBitrate_v25_hook = (getBitrate_v25)Utils::TrampHook32((char*)0x00E3E460, (char*)getBitrate_hook_v25, 6);
|
||||||
//startUpdate_v47_hook = (startUpdate_v47)Utils::TrampHook32((char*)0x009FB530, (char*)startUpdate_hook_v47, 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);
|
//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);
|
//isValidUrl_v25_hook = (isValidUrl_v25)Utils::TrampHook32((char*)0x0105CD80, (char*)isValidUrl_hook_v25, 6);
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
<ProjectGuid>{e6674e55-3cf4-45e4-a22f-7e257bcea583}</ProjectGuid>
|
<ProjectGuid>{e6674e55-3cf4-45e4-a22f-7e257bcea583}</ProjectGuid>
|
||||||
<RootNamespace>SpotifyKeyDumper</RootNamespace>
|
<RootNamespace>SpotifyKeyDumper</RootNamespace>
|
||||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||||
|
<VcpkgTriplet Condition="'$(Platform)'=='Win32'">x86-windows-static-md</VcpkgTriplet>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||||
@ -78,6 +79,7 @@
|
|||||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||||
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
|
<FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
|
||||||
<LanguageStandard>stdcpp14</LanguageStandard>
|
<LanguageStandard>stdcpp14</LanguageStandard>
|
||||||
|
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Windows</SubSystem>
|
<SubSystem>Windows</SubSystem>
|
||||||
@ -85,6 +87,7 @@
|
|||||||
<OptimizeReferences>true</OptimizeReferences>
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
<EnableUAC>false</EnableUAC>
|
<EnableUAC>false</EnableUAC>
|
||||||
|
<AdditionalDependencies>%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
#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 };
|
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})"
|
||||||
|
"\\b([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)";
|
||||||
|
|
||||||
bool Utils::Detour32(char* src, char* dst, const intptr_t len)
|
bool Utils::Detour32(char* src, char* dst, const intptr_t len)
|
||||||
{
|
{
|
||||||
@ -61,7 +63,7 @@ int Utils::GetSpotifyVersion()
|
|||||||
LPBYTE lpVersionInfo = new BYTE[dwFVISize];
|
LPBYTE lpVersionInfo = new BYTE[dwFVISize];
|
||||||
GetFileVersionInfo(lpszFilePath, 0, dwFVISize, lpVersionInfo);
|
GetFileVersionInfo(lpszFilePath, 0, dwFVISize, lpVersionInfo);
|
||||||
UINT uLen;
|
UINT uLen;
|
||||||
VS_FIXEDFILEINFO* lpFfi;
|
VS_FIXEDFILEINFO* lpFfi = {};
|
||||||
VerQueryValue(lpVersionInfo, _T("\\"), (LPVOID*)&lpFfi, &uLen);
|
VerQueryValue(lpVersionInfo, _T("\\"), (LPVOID*)&lpFfi, &uLen);
|
||||||
DWORD dwFileVersionMS = lpFfi->dwFileVersionMS;
|
DWORD dwFileVersionMS = lpFfi->dwFileVersionMS;
|
||||||
DWORD dwFileVersionLS = lpFfi->dwFileVersionLS;
|
DWORD dwFileVersionLS = lpFfi->dwFileVersionLS;
|
||||||
@ -115,30 +117,158 @@ std::wstring Utils::Utf8ToUtf16(const std::string& str)
|
|||||||
return convertedString;
|
return convertedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class FileType
|
||||||
|
{
|
||||||
|
OGG,
|
||||||
|
MP3
|
||||||
|
};
|
||||||
|
|
||||||
struct SongInfo
|
struct SongInfo
|
||||||
{
|
{
|
||||||
std::string title, artist, album, cover;
|
FileType fileType{ FileType::OGG };
|
||||||
} songInfo;
|
std::string title{ std::string() };
|
||||||
|
std::string artist{ std::string() };
|
||||||
|
std::string album{ std::string() };
|
||||||
|
std::string coverUrl{ std::string() };
|
||||||
|
std::string releaseType{ std::string() };
|
||||||
|
std::string releaseDate{ std::string() };
|
||||||
|
std::string isrc{ std::string() };
|
||||||
|
unsigned int year{ 0 };
|
||||||
|
unsigned int trackNum{ 1 };
|
||||||
|
unsigned int totalTracks{ 1 };
|
||||||
|
unsigned int discNum{ 0 };
|
||||||
|
bool isExplicit{ false };
|
||||||
|
};
|
||||||
|
|
||||||
void ClearSongInfo()
|
void TagSong(std::wstring songPath, SongInfo* songInfo)
|
||||||
{
|
{
|
||||||
songInfo.title = "";
|
TagLib::String fileName = songPath.c_str();
|
||||||
songInfo.artist = "";
|
TagLib::String tTitle(songInfo->title, TagLib::String::Type::UTF8);
|
||||||
songInfo.album = "";
|
TagLib::String tArtist(songInfo->artist, TagLib::String::Type::UTF8);
|
||||||
songInfo.cover = "";
|
TagLib::String tAlbum(songInfo->album, TagLib::String::Type::UTF8);
|
||||||
|
TagLib::String tReleaseType(songInfo->releaseType, TagLib::String::Type::UTF8);
|
||||||
|
TagLib::String tReleaseDate(songInfo->releaseDate, TagLib::String::Type::UTF8);
|
||||||
|
TagLib::String tIsrc(songInfo->isrc, TagLib::String::Type::UTF8);
|
||||||
|
TagLib::String tTotalTracks(std::to_string(songInfo->totalTracks), TagLib::String::Type::UTF8);
|
||||||
|
TagLib::String tDiscNum(std::to_string(songInfo->discNum), TagLib::String::Type::UTF8);
|
||||||
|
//TagLib::String tIsExplicit(std::to_string(songInfo->isExplicit), TagLib::String::Type::UTF8);
|
||||||
|
|
||||||
|
// Parse episode URL to separate host and path
|
||||||
|
std::string coverUrlHost, coverUrlPath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::regex re(urlRegex);
|
||||||
|
std::smatch match;
|
||||||
|
if (std::regex_search(songInfo->coverUrl, match, re) && match.size() > 1)
|
||||||
|
{
|
||||||
|
coverUrlHost = match.str(1);
|
||||||
|
coverUrlPath = match.str(2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cout << "Error: Cover art 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string coverArtData = Utils::DownloadSpotifyUrl(coverUrlHost, coverUrlPath, "");
|
||||||
|
TagLib::ByteVector tCoverArtData(reinterpret_cast<const char*>(&coverArtData[0]), coverArtData.length());
|
||||||
|
|
||||||
|
switch (songInfo->fileType)
|
||||||
|
{
|
||||||
|
case FileType::OGG:
|
||||||
|
{
|
||||||
|
TagLib::Ogg::Vorbis::File audioFile(songPath.c_str());
|
||||||
|
TagLib::Ogg::XiphComment* tag = audioFile.tag();
|
||||||
|
|
||||||
|
TagLib::FLAC::Picture* coverArt = new TagLib::FLAC::Picture();
|
||||||
|
coverArt->setType((TagLib::FLAC::Picture::Type)0x03); // Front Cover
|
||||||
|
coverArt->setMimeType("image/jpeg");
|
||||||
|
coverArt->setDescription("Front Cover");
|
||||||
|
coverArt->setData(tCoverArtData);
|
||||||
|
|
||||||
|
tag->addPicture(coverArt);
|
||||||
|
tag->setTitle(tTitle);
|
||||||
|
tag->setArtist(tArtist);
|
||||||
|
tag->setAlbum(tAlbum);
|
||||||
|
tag->setTrack(songInfo->trackNum);
|
||||||
|
tag->setYear(songInfo->year);
|
||||||
|
|
||||||
|
tag->addField("DATE", tReleaseDate);
|
||||||
|
tag->addField("DISCNUMBER", tDiscNum);
|
||||||
|
tag->addField("ISRC", tIsrc);
|
||||||
|
tag->addField("SOURCEMEDIA", "Digital Media");
|
||||||
|
tag->addField("RELEASETYPE", tReleaseType);
|
||||||
|
tag->addField("TOTALTRACKS", tTotalTracks);
|
||||||
|
|
||||||
|
audioFile.save();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FileType::MP3:
|
||||||
|
{
|
||||||
|
TagLib::MPEG::File audioFile(songPath.c_str());
|
||||||
|
TagLib::ID3v2::Tag* tag = audioFile.ID3v2Tag(true);
|
||||||
|
|
||||||
|
tag->setTitle(tTitle);
|
||||||
|
tag->setArtist(tArtist);
|
||||||
|
tag->setAlbum(tAlbum);
|
||||||
|
tag->setTrack(songInfo->trackNum);
|
||||||
|
tag->setYear(songInfo->year);
|
||||||
|
|
||||||
|
TagLib::ID3v2::AttachedPictureFrame* frame = new TagLib::ID3v2::AttachedPictureFrame;
|
||||||
|
if (frame->picture().size() < tCoverArtData.size())
|
||||||
|
{
|
||||||
|
frame->setMimeType("image/jpeg");
|
||||||
|
frame->setPicture(tCoverArtData);
|
||||||
|
tag->addFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
audioFile.save();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int GetUrlNum(int quality)
|
||||||
|
{
|
||||||
|
switch (quality)
|
||||||
|
{
|
||||||
|
case 0: // Automatic
|
||||||
|
return 1; // Set to high quality
|
||||||
|
case 1: // Low
|
||||||
|
return 8;
|
||||||
|
case 2: // Normal
|
||||||
|
return 0;
|
||||||
|
case 3: // High
|
||||||
|
return 1;
|
||||||
|
case 4: // Very high
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
return 1; // Shouldn't happen; set to high quality
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const std::string songRegex =
|
|
||||||
"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::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 uri, std::string key, std::string authToken)
|
void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, std::string authToken, int quality)
|
||||||
{
|
{
|
||||||
std::string downloadStr;
|
std::string downloadStr;
|
||||||
std::wstring songExtension = L".ogg";
|
std::wstring songExtension = L".ogg";
|
||||||
|
|
||||||
|
SongInfo* songInfo = new SongInfo();
|
||||||
|
/*songInfo->title = "";
|
||||||
|
songInfo->artist = "";
|
||||||
|
songInfo->album = "";
|
||||||
|
songInfo->coverUrl = "";
|
||||||
|
songInfo->fileType = FileType::OGG;*/
|
||||||
|
|
||||||
if (fileId.empty() || uri.empty() || authToken.empty())
|
if (fileId.empty() || uri.empty() || authToken.empty())
|
||||||
{
|
{
|
||||||
std::cout << "Could not download song or episode: missing fileId, trackUri, or authToken!" << std::endl;
|
std::cout << "Could not download song or episode: missing fileId, trackUri, or authToken!" << std::endl;
|
||||||
@ -157,8 +287,9 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get storage resolve from Spotify
|
// Get storage resolve from Spotify
|
||||||
|
std::string urlNum = std::to_string(GetUrlNum(quality));
|
||||||
std::string srStr = DownloadSpotifyUrl("spclient.wg.spotify.com",
|
std::string srStr = 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)
|
if (srStr.length() <= 5)
|
||||||
{
|
{
|
||||||
@ -177,7 +308,7 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
|||||||
std::string songPath;
|
std::string songPath;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::regex re(songRegex);
|
std::regex re(urlRegex);
|
||||||
std::smatch match;
|
std::smatch match;
|
||||||
if (std::regex_search(srStr, match, re) && match.size() > 1)
|
if (std::regex_search(srStr, match, re) && match.size() > 1)
|
||||||
{
|
{
|
||||||
@ -206,11 +337,16 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadStr.substr(0, 6).compare("<HTML>") == 0)
|
if (downloadStr.compare(0, 6, "<HTML>") == 0)
|
||||||
{
|
{
|
||||||
std::cout << "Error: " + downloadStr << std::endl;
|
std::cout << "Error: " + downloadStr << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else if (downloadStr.compare(0, 5, "Error") == 0)
|
||||||
|
{
|
||||||
|
std::cout << downloadStr << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Decrypt encrypted song data with Tiny AES in C
|
// Decrypt encrypted song data with Tiny AES in C
|
||||||
struct AES_ctx ctx;
|
struct AES_ctx ctx;
|
||||||
@ -231,10 +367,21 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
|||||||
std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/tracks/"
|
std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/tracks/"
|
||||||
+ uri.substr(uri.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->coverUrl = strtok((char*)(metadata.substr(metadata.find("height\" :") + 30)).c_str(), "\"");
|
||||||
|
songInfo->releaseType = strtok((char*)(metadata.substr(metadata.find("album_type\" :") + 15)).c_str(), "\"");
|
||||||
|
songInfo->releaseDate = strtok((char*)(metadata.substr(metadata.find("release_date\" :") + 17)).c_str(), "\"");
|
||||||
|
songInfo->isrc = strtok((char*)(metadata.substr(metadata.find("isrc\" :") + 9)).c_str(), "\"");
|
||||||
|
songInfo->trackNum = std::stoi(strtok((char*)(metadata.substr(metadata.find("track_number\" :") + 16)).c_str(),
|
||||||
|
","));
|
||||||
|
songInfo->totalTracks = std::stoi(strtok((char*)(metadata.substr(metadata.find("total_tracks\" :")
|
||||||
|
+ 16)).c_str(), ","));
|
||||||
|
songInfo->discNum = std::stoi(strtok((char*)(metadata.substr(metadata.find("disc_number\" :") + 15)).c_str(),
|
||||||
|
","));
|
||||||
|
songInfo->isExplicit = strtok((char*)(metadata.substr(metadata.find("explicit\" :") + 12)).c_str(), ",");
|
||||||
|
songInfo->fileType = FileType::OGG;
|
||||||
|
|
||||||
songExtension = L".ogg";
|
songExtension = L".ogg";
|
||||||
}
|
}
|
||||||
@ -249,7 +396,7 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
|||||||
// Parse episode URL to separate host and path
|
// Parse episode URL to separate host and path
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::regex re(songRegex);
|
std::regex re(urlRegex);
|
||||||
std::smatch match;
|
std::smatch match;
|
||||||
if (std::regex_search(episodeUrl, match, re) && match.size() > 1)
|
if (std::regex_search(episodeUrl, match, re) && match.size() > 1)
|
||||||
{
|
{
|
||||||
@ -284,46 +431,50 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
|||||||
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);
|
||||||
|
|
||||||
songInfo.title = strtok((char*)(metadata.substr(metadata.find("name") + 9)).c_str(), "\"");
|
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->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.cover = 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;
|
||||||
|
|
||||||
songExtension = L".mp3";
|
songExtension = L".mp3";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (songInfo.title.empty() || songInfo.artist.empty() || songInfo.album.empty())
|
if (songInfo->title.empty() || songInfo->artist.empty() || songInfo->album.empty())
|
||||||
{
|
{
|
||||||
std::cout << "Error: Invalid title/artist/album name!" << std::endl;
|
std::cout << "Error: Invalid title/artist/album name!" << std::endl;
|
||||||
|
delete songInfo;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring tempDirArtist = FixPathStr(Utf8ToUtf16(songInfo.artist));
|
std::wstring tempDirArtist = FixPathStr(Utf8ToUtf16(songInfo->artist));
|
||||||
|
|
||||||
songDir = songDirRoot;
|
songDir = songDirRoot;
|
||||||
if (!CreateDirectoryW(songDir.c_str(), NULL) && ERROR_ALREADY_EXISTS != GetLastError())
|
if (!CreateDirectoryW(songDir.c_str(), NULL) && ERROR_ALREADY_EXISTS != GetLastError())
|
||||||
std::cout << "Couldn't create main downloads directory!" << std::endl;
|
std::cout << "Couldn't create main downloads directory!" << std::endl;
|
||||||
|
else if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist).c_str(), NULL)
|
||||||
if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist).c_str(), NULL)
|
|
||||||
|| ERROR_ALREADY_EXISTS == GetLastError())
|
|| ERROR_ALREADY_EXISTS == GetLastError())
|
||||||
{
|
{
|
||||||
std::wstring tempDirAlbum = FixPathStr(Utf8ToUtf16(songInfo.album));
|
std::wstring tempDirAlbum = FixPathStr(Utf8ToUtf16(songInfo->album));
|
||||||
|
|
||||||
if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist + std::wstring(L"\\")
|
if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist + std::wstring(L"\\")
|
||||||
+ tempDirAlbum).c_str(), NULL) || ERROR_ALREADY_EXISTS == GetLastError())
|
+ tempDirAlbum).c_str(), NULL) || ERROR_ALREADY_EXISTS == GetLastError())
|
||||||
{
|
{
|
||||||
songDir += L"\\" + tempDirArtist + std::wstring(L"\\") + tempDirAlbum;
|
std::wstring tempDirSong = FixPathStr(Utf8ToUtf16(songInfo->title));
|
||||||
|
|
||||||
std::wstring tempDirSong = FixPathStr(Utf8ToUtf16(songInfo.title));
|
songDir += L"\\" + tempDirArtist + std::wstring(L"\\") + tempDirAlbum + L".\\" + tempDirArtist + L" - "
|
||||||
|
+ tempDirSong + songExtension;
|
||||||
|
|
||||||
std::ofstream songFileOut(songDir + L".\\" + tempDirArtist + L" - " + tempDirSong + songExtension,
|
std::ofstream songFileOut(songDir, std::ios_base::binary);
|
||||||
std::ios_base::binary);
|
|
||||||
songFileOut.write(downloadStr.c_str(), downloadStr.size());
|
songFileOut.write(downloadStr.c_str(), downloadStr.size());
|
||||||
songFileOut.close();
|
songFileOut.close();
|
||||||
|
|
||||||
std::cout << "Finished downloading: " << songInfo.artist << " - \"" << songInfo.title << "\"!" << std::endl;
|
TagSong(songDir, songInfo);
|
||||||
|
|
||||||
ClearSongInfo();
|
std::cout << "Finished downloading: " << songInfo->artist << " - \"" << songInfo->title << "\"!"
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
|
delete songInfo;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -335,29 +486,6 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s
|
|||||||
{
|
{
|
||||||
std::cout << "Couldn't create artist directory!" << std::endl;
|
std::cout << "Couldn't create artist directory!" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Could not finish downloading song!" << std::endl;
|
|
||||||
|
|
||||||
ClearSongInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string GetLastErrorAsString()
|
|
||||||
{
|
|
||||||
//Get the error message, if any.
|
|
||||||
DWORD errorMessageID = ::GetLastError();
|
|
||||||
if (errorMessageID == 0)
|
|
||||||
return std::string(); //No error message has been recorded
|
|
||||||
|
|
||||||
LPSTR messageBuffer = nullptr;
|
|
||||||
size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
||||||
NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);
|
|
||||||
|
|
||||||
std::string message(messageBuffer, size);
|
|
||||||
|
|
||||||
//Free the buffer.
|
|
||||||
LocalFree(messageBuffer);
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@ -373,26 +501,26 @@ std::string Utils::DownloadSpotifyUrl(std::string host, std::string path, std::s
|
|||||||
hSession = InternetOpenA(userAgent.c_str(), INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
|
hSession = InternetOpenA(userAgent.c_str(), INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
|
||||||
|
|
||||||
if (hSession == NULL)
|
if (hSession == NULL)
|
||||||
return "Error: Could not initialize request!";
|
return "Error: Could not initialize request: " + GetLastError();
|
||||||
|
|
||||||
hConnect = InternetConnectA(hSession, host.c_str(), 80, NULL, NULL, INTERNET_SERVICE_HTTP, 0,
|
hConnect = InternetConnectA(hSession, host.c_str(), 80, NULL, NULL, INTERNET_SERVICE_HTTP, 0,
|
||||||
NULL);
|
NULL);
|
||||||
|
|
||||||
if (hConnect == NULL)
|
if (hConnect == NULL)
|
||||||
return "Error: Could not create connect!";
|
return "Error: Could not create connect: " + GetLastError();
|
||||||
|
|
||||||
hRequest = HttpOpenRequestA(hConnect, "GET", path.c_str(), NULL, NULL, NULL, INTERNET_FLAG_NO_AUTH, 0);
|
hRequest = HttpOpenRequestA(hConnect, "GET", path.c_str(), NULL, NULL, NULL, INTERNET_FLAG_NO_AUTH, 0);
|
||||||
|
|
||||||
if (hRequest == NULL)
|
if (hRequest == NULL)
|
||||||
return "Error: Could not create open request!";
|
return "Error: Could not create open request: " + GetLastError();
|
||||||
|
|
||||||
HttpAddRequestHeadersA(hRequest, authHeader.c_str(), -1, HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE);
|
HttpAddRequestHeadersA(hRequest, authHeader.c_str(), -1, HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE);
|
||||||
bRequestSent = HttpSendRequestA(hRequest, NULL, 0, NULL, 0);
|
bRequestSent = HttpSendRequestA(hRequest, NULL, 0, NULL, 0);
|
||||||
|
|
||||||
if (!bRequestSent)
|
if (!bRequestSent)
|
||||||
return "Error: Could not send request!";
|
return "Error: Could not send request: " + GetLastError();
|
||||||
|
|
||||||
char tmpBuffer[bufferSize];
|
char tmpBuffer[bufferSize] = {};
|
||||||
BOOL canRead = true;
|
BOOL canRead = true;
|
||||||
DWORD bytesRead = -1;
|
DWORD bytesRead = -1;
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ public:
|
|||||||
static std::string HexString(BYTE* data, int len);
|
static std::string HexString(BYTE* data, int len);
|
||||||
static std::wstring FixPathStr(std::wstring str);
|
static std::wstring FixPathStr(std::wstring str);
|
||||||
static std::wstring Utf8ToUtf16(const std::string& str);
|
static std::wstring Utf8ToUtf16(const std::string& str);
|
||||||
static void DownloadSong(std::string fileId, std::string fileUri, std::string key, std::string authToken);
|
static void DownloadSong(std::string fileId, std::string uri, std::string key, std::string authToken, int quality);
|
||||||
static std::string DownloadSpotifyUrl(std::string host, std::string path, std::string authToken);
|
static std::string DownloadSpotifyUrl(std::string host, std::string path, std::string authToken);
|
||||||
static bool BadPtr(void* ptr);
|
static bool BadPtr(void* ptr);
|
||||||
};
|
};
|
||||||
|
@ -25,4 +25,11 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <WinInet.h>
|
#include <WinInet.h>
|
||||||
|
|
||||||
|
// TagLib
|
||||||
|
#define TAGLIB_STATIC
|
||||||
|
#include <taglib/mpegfile.h>
|
||||||
|
#include <taglib/attachedpictureframe.h>
|
||||||
|
#include <taglib/id3v2tag.h>
|
||||||
|
#include <taglib/vorbisfile.h>
|
||||||
|
|
||||||
#endif //PCH_H
|
#endif //PCH_H
|
||||||
|
Loading…
Reference in New Issue
Block a user