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
|
||||
### 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).
|
||||
|
||||
Now with automatic download support (compatible versions listed below)!
|
||||
|
||||
* 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
|
||||
**Now with automatic download support for songs and podcast episodes!**
|
||||
|
||||
## Using
|
||||
1. Make sure `SpotifyKeyDumperInjector.exe` and `SpotifyKeyDumper.dll` are located in the same folder as Spotify (`Spotify.exe`).
|
||||
2. Start SpotifyKeyDumperInjector before launching Spotify.
|
||||
1. Go to `%appdata%\Spotify` (or wherever your Spotify installation is located)
|
||||
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
|
||||
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
|
||||
* Inspired by XSpotify
|
||||
|
@ -27,10 +27,20 @@ signalEmitter_v45 signalEmitter_v45_hook = nullptr;
|
||||
typedef void (__cdecl* keyDecoder_v47)();
|
||||
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 keyStr = std::string();
|
||||
|
||||
std::string trackUriStr = std::string();
|
||||
std::string uriStr = std::string();
|
||||
__int64 newPosition = 0;
|
||||
bool signalled = false;
|
||||
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,
|
||||
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);
|
||||
}
|
||||
@ -138,20 +148,23 @@ int* __fastcall log_hook_v45(void* This, void* _EDX, int a2, int a3, void* a4, c
|
||||
|
||||
if (!Utils::BadPtr(logChars))
|
||||
{
|
||||
//std::string logStr = std::string(logChars).substr(8, 5);
|
||||
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.compare(19, 13, "spotify:track") == 0)
|
||||
{
|
||||
//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;
|
||||
}
|
||||
// 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
|
||||
{
|
||||
@ -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);
|
||||
}
|
||||
|
||||
std::string lastKey = std::string();
|
||||
std::string lastUri = std::string();
|
||||
void __fastcall fileIdWriter_hook_v45(void* This, void* _EDX, int* a2)
|
||||
{
|
||||
// [[ebp+8]+28]
|
||||
char* fileId = (char*) *(DWORD*)(a2 + 16); // 0x40 / 4 = 16
|
||||
|
||||
//std::cout << "fileId: " << fileId << std::endl << std::endl;
|
||||
|
||||
if (signalled && lastKey.compare(keyStr) != 0)
|
||||
if (signalled && lastUri.compare(uriStr) != 0)
|
||||
{
|
||||
//std::cout << "signalled = false" << std::endl;
|
||||
signalled = false;
|
||||
lastKey = keyStr;
|
||||
std::thread t2(Utils::DownloadSong, std::string(fileId), trackUriStr, keyStr, authToken);
|
||||
lastUri = uriStr;
|
||||
std::thread t2(Utils::DownloadSong, std::string(fileId), uriStr, keyStr, authToken);
|
||||
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()
|
||||
{
|
||||
BYTE ref_v19 = 0x55;
|
||||
@ -330,9 +388,9 @@ void Hooks::Init()
|
||||
case 45:
|
||||
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);
|
||||
//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);
|
||||
fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CBB560, (char*)&fileIdWriter_hook_v45,
|
||||
//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);
|
||||
fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CBB560, (char*)fileIdWriter_hook_v45,
|
||||
5);
|
||||
signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00B095A0, (char*)signalEmitter_hook_v45,
|
||||
5);
|
||||
@ -340,8 +398,8 @@ void Hooks::Init()
|
||||
case 46:
|
||||
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);
|
||||
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,
|
||||
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,
|
||||
5);
|
||||
signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00B02270, (char*)signalEmitter_hook_v45,
|
||||
5);
|
||||
@ -349,13 +407,16 @@ void Hooks::Init()
|
||||
case 47:
|
||||
keyBuffer_v47 = new char[16]; // 128 bits = 16 bytes
|
||||
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);
|
||||
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,
|
||||
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,
|
||||
5);
|
||||
signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00AFBB50, (char*)signalEmitter_hook_v45,
|
||||
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;
|
||||
}
|
||||
}
|
@ -120,19 +120,39 @@ struct SongInfo
|
||||
std::string title, artist, album, cover;
|
||||
} songInfo;
|
||||
|
||||
void ClearSongInfo()
|
||||
{
|
||||
songInfo.title = "";
|
||||
songInfo.artist = "";
|
||||
songInfo.album = "";
|
||||
songInfo.cover = "";
|
||||
}
|
||||
|
||||
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::wstring songDirRoot = L"Downloads";
|
||||
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::string downloadStr;
|
||||
std::wstring songExtension = L".ogg";
|
||||
|
||||
if (fileId.empty() || uri.empty() || authToken.empty())
|
||||
{
|
||||
std::cout << "Could not download song or episode: missing fileId, trackUri, or authToken!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Is length check even needed for compare()?
|
||||
if (uri.length() > 13 && uri.compare(0, 13, "spotify:track") == 0)
|
||||
{
|
||||
std::cout << "Downloading song..." << std::endl;
|
||||
|
||||
if (fileId.empty() || trackUri.empty() || key.empty() || authToken.empty())
|
||||
if (key.empty())
|
||||
{
|
||||
std::cout << "Could not download song: missing fileId, trackUri, key, or authToken!" << std::endl;
|
||||
std::cout << "Could not download song: missing key!";
|
||||
return;
|
||||
}
|
||||
|
||||
@ -173,43 +193,96 @@ void Utils::DownloadSong(std::string fileId, std::string trackUri, std::string k
|
||||
catch (std::regex_error& e)
|
||||
{
|
||||
// Syntax error in the regular expression
|
||||
std::cout << "Error: regex_error" << std::endl;
|
||||
std::cout << "Error: Invalid regex!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// Download encrypted song data from Spotify
|
||||
std::string songStr = DownloadSpotifyUrl(songHost, songPath, "");
|
||||
downloadStr = DownloadSpotifyUrl(songHost, songPath, "");
|
||||
|
||||
if (songStr.length() <= 6)
|
||||
if (downloadStr.length() <= 6)
|
||||
{
|
||||
std::cout << "Error: Could not download audio!" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (songStr.substr(0, 6).compare("<HTML>") == 0)
|
||||
if (downloadStr.substr(0, 6).compare("<HTML>") == 0)
|
||||
{
|
||||
std::cout << "Error: " + songStr << std::endl;
|
||||
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*>(&songStr[0]), songStr.size());
|
||||
AES_CTR_xcrypt_buffer(&ctx, reinterpret_cast<uint8_t*>(&downloadStr[0]), downloadStr.size());
|
||||
|
||||
// Remove custom Spotify Ogg page from beginning of file
|
||||
songStr = songStr.substr(songStr.find("\xFF\xFF\xFF\xFFOggS") + 4);
|
||||
downloadStr = downloadStr.substr(downloadStr.find("\xFF\xFF\xFF\xFFOggS") + 4);
|
||||
|
||||
if (!trackUri.empty())
|
||||
{
|
||||
// Download song metadata from Spotify API
|
||||
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.artist = strtok((char*)(metadata.substr(metadata.find("name") + 9)).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(), "\"");
|
||||
|
||||
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;
|
||||
|
||||
std::cout << "Downloading episode..." << std::endl;
|
||||
|
||||
// Parse episode URL to separate host and path
|
||||
try
|
||||
{
|
||||
std::regex re(songRegex);
|
||||
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 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;
|
||||
@ -228,12 +301,14 @@ void Utils::DownloadSong(std::string fileId, std::string trackUri, std::string k
|
||||
|
||||
std::wstring tempDirSong = FixPathStr(Utf8ToUtf16(songInfo.title));
|
||||
|
||||
std::ofstream songFileOut(songDir + L".\\" + tempDirArtist + L" - " + tempDirSong + L".ogg",
|
||||
std::ofstream songFileOut(songDir + L".\\" + tempDirArtist + L" - " + tempDirSong + songExtension,
|
||||
std::ios_base::binary);
|
||||
songFileOut.write(songStr.c_str(), songStr.size());
|
||||
songFileOut.write(downloadStr.c_str(), downloadStr.size());
|
||||
songFileOut.close();
|
||||
|
||||
std::cout << "Finished downloading: " << songInfo.artist << " - \"" << songInfo.title << "\"!" << std::endl;
|
||||
|
||||
ClearSongInfo();
|
||||
return;
|
||||
}
|
||||
else
|
||||
@ -247,7 +322,8 @@ void Utils::DownloadSong(std::string fileId, std::string trackUri, std::string k
|
||||
}
|
||||
|
||||
std::cout << "Could not finish downloading song!" << std::endl;
|
||||
}
|
||||
|
||||
ClearSongInfo();
|
||||
}
|
||||
|
||||
std::string GetLastErrorAsString()
|
||||
|
@ -11,6 +11,7 @@
|
||||
#pragma comment(lib, "Version.lib")
|
||||
#pragma comment(lib, "Wininet.lib")
|
||||
|
||||
#include <codecvt>
|
||||
#include <cstdint>
|
||||
#include "framework.h"
|
||||
#include <fstream>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 842 KiB |
Loading…
Reference in New Issue
Block a user