Skip to content

Commit

Permalink
New install approach, misc fixes
Browse files Browse the repository at this point in the history
- Persist on main Spotify install and %localappdata% for config
- DLL hijacking instead of injection
- Fix Liked Songs playlist bricking

BlockTheSpot also targets dpapi.dll for hijacking, so Soggfy won't be
compatible with it. dpapi is the easiest target because it doesn't need
export redirection (at least of yet).

netmsg.dll may be another possible target, but I'm not sure it's quite
reliable since it seems to be loaded exclusively for some error message
formatting stuff.
  • Loading branch information
Rafiuth committed Dec 8, 2023
1 parent 9f7cbc9 commit 3428da3
Show file tree
Hide file tree
Showing 21 changed files with 220 additions and 252 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ jobs:
npm install
npm run build
Copy-Item dist/bundle.js ../build/Release/Soggfy.js
Copy-Item dist/bundle.js.map ../build/Release/Soggfy.js.map
Copy-Item dist/*.* ../build/Release/
cd ../
- name: Create Zip
Expand Down
26 changes: 4 additions & 22 deletions Injector/Injector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,20 +181,7 @@ void DeleteSpotifyUpdate()
}
CoTaskMemFree(localAppData);
}
bool IsFFmpegInstalled()
{
//Try Soggfy/ffmpeg/ffmpeg.exe
if (fs::exists("ffmpeg/ffmpeg.exe")) {
return true;
}
//Try %PATH%
std::wstring envPath;
DWORD envPathLen = SearchPath(NULL, L"ffmpeg.exe", NULL, 0, envPath.data(), NULL);
if (envPathLen != 0) {
return true;
}
return false;
}

void EnableAnsiColoring()
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
Expand All @@ -205,18 +192,18 @@ void EnableAnsiColoring()

int main(int argc, char* argv[])
{
bool attach = false;
bool launch = false;
bool enableRemoteDebug = false;
for (int i = 0; i < argc; i++) {
attach |= strcmp(argv[i], "-a") == 0;
launch |= strcmp(argv[i], "-l") == 0;
enableRemoteDebug |= strcmp(argv[i], "-d") == 0;
}

EnableAnsiColoring();

try {
HANDLE targetProc;
if (attach) {
if (!launch) {
targetProc = FindSpotifyProcess();
} else {
KillSpotifyProcesses();
Expand All @@ -229,11 +216,6 @@ int main(int argc, char* argv[])
CloseHandle(targetProc);

std::cout << COL_GREEN "Injection succeeded!\n" COL_RESET;

if (!IsFFmpegInstalled()) {
std::cout << COL_YELLOW "Note: FFmpeg binaries were not found, songs won't be tagged nor converted.\n";
std::cout << COL_YELLOW "Run Install.ps1 or add ffmpeg to the PATH environment variable.\n" COL_RESET;
}
} catch (std::exception& ex) {
std::cout << COL_RED "Error: " << ex.what() << "\n" COL_RESET;
}
Expand Down
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,34 @@ A music downloader mod for the Windows Spotify client
# Installation and Usage
1. Download and extract the `.zip` package of the [latest release](https://github.com/Rafiuth/Soggfy/releases/latest).
2. Double click the `Install.cmd` file. It will run the Install.ps1 script with Execution Policy Bypass. Wait for it to finish.
3. Run `Injector.exe`, and wait for Spotify to open.
4. Play the songs you want to download.
3. Open Spotify and play the songs you want to download.

Tracks are saved on the Music folder by default, this can be changed on the settings pane, which can be accessed through the controls button shown after hovering the download toggle button in the navigation bar.
Tracks are saved in the Music folder by default. The settings panel can be accessed by hovering next to the download button in the navigation bar.
Hovering the check mark drawn on each individual track will display a popup offering to open the folder containing them.

You may need to disable or whitelist Soggfy in your anti-virus for it to work.

If the injector crashes because of missing DLLs, you need to install the [MSVC Redistributable package](https://aka.ms/vs/17/release/vc_redist.x86.exe).
If the Spotify client crashes because of missing DLLs, you may need to install the [MSVC Redistributable package](https://aka.ms/vs/17/release/vc_redist.x86.exe).

# Notes
- Songs are only downloaded if played from start to finish, without seeking (pausing is fine).
- Quality depends on the account being used: _160Kb/s_ or _320Kb/s_ for _free_ and _premium_ plans respectively. You may also need to change the streaming quality to "Very high" on Spotify settings to get _320Kb/s_ files.
- If you are _converting_ to AAC and care about quality, see [High quality AAC](/USAGE.md#high-quality-aac).
- **This tool breaks [Spotify's Guidelines](https://www.spotify.com/us/legal/user-guidelines/) and using it could get your account banned. Consider using alt accounts or keeping backups (see [Exportify](https://github.com/watsonbox/exportify) and [SpotMyBackup](http:https://www.spotmybackup.com)).**
- Podcast support is very hit or miss and will only work with audio-only OGG podcasts (usually the exclusive ones).
- **This mod breaks [Spotify's Guidelines](https://www.spotify.com/us/legal/user-guidelines/) and using it could get your account banned. Consider using alt accounts or keeping backups (see [Exportify](https://github.com/watsonbox/exportify) and [SpotMyBackup](http:https://www.spotmybackup.com)).**

# How it works
Soggfy works by intercepting Spotify's OGG parser and capturing the unencr​ypted data during playback. This process is similar to recording, but it results in an exact copy of the original file served by Spotify, without ever extracting k​eys or actually re-downloading it.
Soggfy works by intercepting Spotify's OGG parser and capturing the unencrypted data during playback. This process is similar to recording, but it results in an exact copy of the original files served by Spotify, without ever extracting keys or actually re-downloading them.
Conversion and metadata is then applied according to user settings.

# Manual Install
# Manual Installation
If you are having issues with the install script, try following the steps below for a manual installation:
1. Download and install the _correct_ Spotify client version using the link inside the install script.
2. Go to `%appdata%` and copy the `Spotify` folder to the `Soggfy` folder (such that the final structure looks like `Soggfy/Spotify/Spotify.exe`).
_Note that you can also install other mods such as Spicetify or SpotX before this step._
3. [Download and install FFmpeg](/USAGE.md#high-quality-aac).

1. Download and install the _correct_ Spotify client version using the link inside the Install.ps1 script.
2. Copy and rename `SpotifyOggDumper.dll` to `%appdata%/Spotify/dpapi.dll`
3. Copy `SoggfyUIC.js` to `%appdata%/Spotify/SoggfyUIC.js`
4. Download and extract [FFmpeg binaries](https://github.com/AnimMouse/ffmpeg-autobuild/releases) to `%localappdata%/Soggfy/ffmpeg/ffmpeg.exe` (or add them to `%PATH%`).

Alternatively, Soggfy can be injected into a running Spotify process by running `Injector.exe`.

# Credits
- [XSpotify](https://web.archive.org/web/20200303145624/https://github.com/meik97/XSpotify) and spotifykeydumper - Inspiration for this project
Expand Down
33 changes: 8 additions & 25 deletions SpotifyOggDumper/CefUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,6 @@

#include <type_traits>

//Compat stuff (these got removed in C++20)
namespace std
{
template <class> struct result_of;
template <class F, class... ArgTypes>
struct result_of<F(ArgTypes...)> : std::invoke_result<void, F, ArgTypes...> {};
}

//hack to prevent linking to cef library
#ifndef NDEBUG
#define NDEBUG
Expand Down Expand Up @@ -41,9 +33,7 @@ namespace std

//Because of the API incompat: trying to free CefString returned by an API will result in a crash.
//This workaround will obviously leak memory, but it's small and rare enough that it isn't a big deal.
#define CEF_STRING_ABANDON(str) \
str.clear(); \
*(bool*)((void**)&str + 2) = false; //url.owner_ = false
#define CEF_STRING_ABANDON(str) url.Detach()

struct _CefBrowserInfo;
typedef void* _CefLock; //ABI: pthreads=size_t(4/8) vs msvc=CRITICAL_SECTION(24/40)
Expand Down Expand Up @@ -199,21 +189,16 @@ namespace CefUtils
}
}

typedef cef_urlrequest_t* (*cef_urlrequest_create_proc)(
_cef_request_t* request,
_cef_urlrequest_client_t* client,
_cef_request_context_t* request_context
);
cef_urlrequest_create_proc UrlRequestCreate_Orig;
std::function<bool(std::wstring_view)> _isUrlBlocked;

cef_urlrequest_t* UrlRequestCreate_Detour(
DETOUR_FUNC(__cdecl, cef_urlrequest_t*, UrlRequestCreate, (
_cef_request_t* request,
_cef_urlrequest_client_t* client,
_cef_request_context_t* request_context)
_cef_request_context_t* request_context
))
{
auto url = request->get_url(request);
auto urlsv = std::wstring_view(url->str, url->length);
auto urlsv = std::wstring_view((wchar_t*)url->str, url->length);
bool blocked = _isUrlBlocked(urlsv);

if (LogMinLevel <= LOG_TRACE) {
Expand All @@ -222,12 +207,10 @@ namespace CefUtils
cef_string_userfree_free(url);
return blocked ? nullptr : UrlRequestCreate_Orig(request, client, request_context);
}
std::pair<OpaqueFn, OpaqueFn*> InitUrlBlocker(std::function<bool(std::wstring_view)> isUrlBlocked)

void InitUrlBlocker(std::function<bool(std::wstring_view)> isUrlBlocked)
{
_isUrlBlocked = isUrlBlocked;
return std::make_pair(
(OpaqueFn)&UrlRequestCreate_Detour,
(OpaqueFn*)&UrlRequestCreate_Orig
);
Hooks::CreateApi(L"libcef.dll", "cef_urlrequest_create", &UrlRequestCreate_Detour, (Hooks::FuncAddr*)&UrlRequestCreate_Orig);
}
}
5 changes: 2 additions & 3 deletions SpotifyOggDumper/CefUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace CefUtils
{
void InjectJS(const std::string& code);

typedef void* OpaqueFn;
//Initializes the URL blocker and returns a pair of <Detour, OrigFunc> for cef_urlrequest_create() hook.
std::pair<OpaqueFn, OpaqueFn*> InitUrlBlocker(std::function<bool(std::wstring_view)> isUrlBlocked);
//Initializes the URL blocker hook.
void InitUrlBlocker(std::function<bool(std::wstring_view)> isUrlBlocked);
}
23 changes: 15 additions & 8 deletions SpotifyOggDumper/ControlServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ void ControlServer::Run()
{
_loop = uWS::Loop::get();
_app = std::make_unique<uWS::App>();
CreateIdleTimer();

_app->ws<Connection>("/sgf_ctrl", {
.compression = uWS::CompressOptions::DISABLED,
Expand Down Expand Up @@ -54,20 +55,12 @@ void ControlServer::Run()
ws->getUserData()->Socket = nullptr;
}
});
#if _DEBUG
_app->get("/", [](auto res, auto req) {
res->end("Soggfy it's working ;)");
});
#endif

int attemptNum = 0;
std::function<void(us_listen_socket_t*)> listenHandler = [&](auto socket) {
if (socket) {
_socket = socket;
LogInfo("Control server listening on port {}", _port);

std::string addr = "ws:https://127.0.0.1:" + std::to_string(_port) + "/sgf_ctrl";
_msgHandler(nullptr, { MessageType::SERVER_OPEN, { { "addr", addr } } });
} else {
if (attemptNum > 64) {
throw std::exception("Failed to open control server.");
Expand All @@ -94,6 +87,7 @@ void ControlServer::Stop()
for (auto ws : clients) {
ws->end(1001, "Server is shutting down");
}
us_timer_close(_idleTimer);
us_listen_socket_close(0, _socket);
_socket = nullptr;
});
Expand All @@ -102,6 +96,19 @@ void ControlServer::Stop()
}
}

void ControlServer::CreateIdleTimer()
{
_idleTimer = us_create_timer((us_loop_t*)_loop, 1, sizeof(ControlServer*));
*(ControlServer**)us_timer_ext(_idleTimer) = this;

us_timer_set(_idleTimer, [](us_timer_t* timer) {
auto sv = *(ControlServer**)us_timer_ext(timer);
if (sv->_clients.size() == 0) {
sv->_msgHandler(nullptr, { MessageType::IDLE });
}
}, 1000, 1000);
}

void SendData(WebSocket* socket, const std::string& data, uWS::OpCode opcode = uWS::BINARY)
{
if (socket->send(data, opcode) != WebSocket::SendStatus::SUCCESS) {
Expand Down
9 changes: 7 additions & 2 deletions SpotifyOggDumper/ControlServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ enum class MessageType
//Internal
HELLO = -1, //Client connected
BYE = -2, //Client disconnected
SERVER_OPEN = -128 //Server listen success, message: { addr: string }
IDLE = -3, //Sent periodicaly when there are no clients connected
};

struct Message
Expand Down Expand Up @@ -87,18 +87,23 @@ class ControlServer
//Sends the specified message to all connected clients. Can be called from any thread.
void Broadcast(const Message& msg);
void Broadcast(MessageType type, const json& content) { Broadcast({ type, content }); }


int GetListenPort() const { return _port; }

private:
std::unique_ptr<uWS::App> _app;
uWS::Loop* _loop = nullptr;
us_listen_socket_t* _socket = nullptr;
us_timer_t* _idleTimer = nullptr;
std::unordered_set<WebSocket*> _clients;

std::condition_variable _doneCond;
std::mutex _doneMutex;

int _port = 28653;
MessageHandler _msgHandler;

void CreateIdleTimer();
};

struct Connection
Expand Down
Loading

0 comments on commit 3428da3

Please sign in to comment.