Compare commits

..

1 Commits

Author SHA1 Message Date
Jonas Kvinge
53d5d06222 Remove discord-rpc from 3rdparty 2025-12-29 00:43:52 +01:00
65 changed files with 37 additions and 7836 deletions

View File

@@ -81,7 +81,7 @@ jobs:
gtest
gmock
sparsehash-devel
rapidjson-devel
discord-rpc-devel
- name: Install kdsingleapplication-qt6-devel
if: matrix.opensuse_version != 'leap:15.6'
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
@@ -207,7 +207,7 @@ jobs:
gtest-devel
gmock-devel
sparsehash-devel
rapidjson-devel
discord-rpc-devel
- name: Checkout
uses: actions/checkout@v6
with:
@@ -303,7 +303,7 @@ jobs:
appstream
appstream-util
hicolor-icon-theme
rapidjson
discord-rpc-devel
- name: Remove files
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
- name: Checkout
@@ -399,7 +399,7 @@ jobs:
appstream-util
hicolor-icon-theme
gtest
rapidjson
discord-rpc-devel
- name: Build and install KDSingleApplication
if: matrix.mageia_version == '9'
run: |
@@ -520,7 +520,7 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON -DENABLE_DISCORD_RPC=OFF
- name: Delete build directory
run: rm -rf build
- name: make deb
@@ -616,7 +616,7 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON -DENABLE_DISCORD_RPC=OFF
- name: Delete build directory
run: rm -rf build
- name: make deb
@@ -692,7 +692,6 @@ jobs:
gstreamer1.0-alsa
gstreamer1.0-pulseaudio
libkdsingleapplication-qt6-dev
rapidjson-dev
- name: Install keyboxd
if: matrix.ubuntu_version == 'noble'
env:
@@ -712,7 +711,7 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON -DENABLE_DISCORD_RPC=OFF
- name: Delete build directory
run: rm -rf build
- name: Import Ubuntu PPA GPG private key
@@ -756,7 +755,7 @@ jobs:
set -e
git config --global --add safe.directory ${GITHUB_WORKSPACE}
cmake -E make_directory build
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug"
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug" -DENABLE_DISCORD_RPC=OFF
cmake --build build --config Debug --parallel 4
@@ -776,7 +775,7 @@ jobs:
with:
usesh: true
mem: 4096
prepare: pkg_add git cmake pkgconf boost glib2 qt6-qtbase qt6-qttools sqlite gstreamer1 gstreamer1-plugins-base chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf libgpod fftw3 icu4c kdsingleapplication pulseaudio sparsehash rapidjson
prepare: pkg_add git cmake pkgconf boost glib2 qt6-qtbase qt6-qttools sqlite gstreamer1 gstreamer1-plugins-base chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf libgpod fftw3 icu4c kdsingleapplication pulseaudio sparsehash
run: |
set -e
export LDFLAGS="-L/usr/local/lib"

View File

@@ -1,41 +0,0 @@
set(DISCORD_RPC_SOURCES
discord_rpc.h
discord_register.h
discord_rpc.cpp
discord_rpc_connection.h
discord_rpc_connection.cpp
discord_serialization.h
discord_serialization.cpp
discord_connection.h
discord_backoff.h
discord_msg_queue.h
)
if(UNIX)
list(APPEND DISCORD_RPC_SOURCES discord_connection_unix.cpp)
if(APPLE)
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
add_definitions(-DDISCORD_OSX)
else()
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
add_definitions(-DDISCORD_LINUX)
endif()
endif()
if(WIN32)
list(APPEND DISCORD_RPC_SOURCES discord_connection_win.cpp discord_register_win.cpp)
add_definitions(-DDISCORD_WINDOWS)
endif()
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
if(APPLE)
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
endif()
if(WIN32)
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
endif()
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -1,19 +0,0 @@
Copyright 2017 Discord, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,162 +0,0 @@
# Discord RPC
## Fork Notice
This library was slightly modified for Strawberry Music Player with some extra features from the new API and shared library support/more unnecessary components removed. The original repository is [here](https://github.com/discord/discord-rpc)
## Deprecation Notice
This library has been deprecated in favor of Discord's GameSDK. [Learn more here](https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide)
---
This is a library for interfacing your game with a locally running Discord desktop client. It's known to work on Windows, macOS, and Linux. You can use the lib directly if you like, or use it as a guide to writing your own if it doesn't suit your game as is. PRs/feedback welcome if you have an improvement everyone might want, or can describe how this doesn't meet your needs.
Included here are some quick demos that implement the very minimal subset to show current status, and
have callbacks for where a more complete game would do more things (joining, spectating, etc).
## Documentation
The most up to date documentation for Rich Presence can always be found on our [developer site](https://discordapp.com/developers/docs/rich-presence/how-to)! If you're interested in rolling your own native implementation of Rich Presence via IPC sockets instead of using our SDK—hey, you've got free time, right?—check out the ["Hard Mode" documentation](https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md).
## Basic Usage
Zeroith, you should be set up to build things because you are a game developer, right?
First, head on over to the [Discord developers site](https://discordapp.com/developers/applications/me) and make yourself an app. Keep track of `Client ID` -- you'll need it here to pass to the init function.
### Unreal Engine 4 Setup
To use the Rich Presense plugin with Unreal Engine Projects:
1. Download the latest [release](https://github.com/discordapp/discord-rpc/releases) for each operating system you are targeting and the zipped source code
2. In the source code zip, copy the UE plugin—`examples/unrealstatus/Plugins/discordrpc`—to your project's plugin directory
3. At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create an `Include` folder and copy `discord_rpc.h` and `discord_register.h` to it from the zip
4. Follow the steps below for each OS
5. Build your UE4 project
6. Launch the editor, and enable the Discord plugin.
#### Windows
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Win64` folder
- Copy `lib/discord-rpc.lib` and `bin/discord-rpc.dll` from `[RELEASE_ZIP]/win64-dynamic` to the `Win64` folder
#### Mac
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Mac` folder
- Copy `libdiscord-rpc.dylib` from `[RELEASE_ZIP]/osx-dynamic/lib` to the `Mac` folder
#### Linux
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Linux` folder
- Inside, create another folder `x86_64-unknown-linux-gnu`
- Copy `libdiscord-rpc.so` from `[RELEASE_ZIP]/linux-dynamic/lib` to `Linux/x86_64-unknown-linux-gnu`
### Unity Setup
If you're a Unity developer looking to integrate Rich Presence into your game, follow this simple guide to get started towards success:
1. Download the DLLs for any platform that you need from [our releases](https://github.com/discordapp/discord-rpc/releases)
2. In your Unity project, create a `Plugins` folder inside your `Assets` folder if you don't already have one
3. Copy the file `DiscordRpc.cs` from [here](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordRpc.cs) into your `Assets` folder. This is basically your header file for the SDK
We've got our `Plugins` folder ready, so let's get platform-specific!
#### Windows
4. Create `x86` and `x86_64` folders inside `Assets/Plugins/`
5. Copy `discord-rpc-win/win64-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86_64/`
6. Copy `discord-rpc-win/win32-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86/`
7. Click on both DLLs and make sure they are targetting the correct architectures in the Unity editor properties pane
8. Done!
#### MacOS
4. Copy `discord-rpc-osx/osx-dynamic/lib/libdiscord-rpc.dylib` to `Assets/Plugins/`
5. Rename `libdiscord-rpc.dylib` to `discord-rpc.bundle`
6. Done!
#### Linux
4. Copy `discord-rpc-linux/linux-dynamic-lib/libdiscord-rpc.so` to `Assets/Plugins/`
5. Done!
You're ready to roll! For code examples on how to interact with the SDK using the `DiscordRpc.cs` header file, check out [our example](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordController.cs)
### From package
Download a release package for your platform(s) -- they have subdirs with various prebuilt options, select the one you need add `/include` to your compile includes, `/lib` to your linker paths, and link with `discord-rpc`. For the dynamically linked builds, you'll need to ship the associated file along with your game.
### From repo
First-eth, you'll want `CMake`. There's a few different ways to install it on your system, and you should refer to [their website](https://cmake.org/install/). Many package managers provide ways of installing CMake as well.
To make sure it's installed correctly, type `cmake --version` into your flavor of terminal/cmd. If you get a response with a version number, you're good to go!
There's a [CMake](https://cmake.org/download/) file that should be able to generate the lib for you; Sometimes I use it like this:
```sh
cd <path to discord-rpc>
mkdir build
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=<path to install discord-rpc to>
cmake --build . --config Release --target install
```
There is a wrapper build script `build.py` that runs `cmake` with a few different options.
Usually, I run `build.py` to get things started, then use the generated project files as I work on things. It does depend on `click` library, so do a quick `pip install click` to make sure you have it if you want to run `build.py`.
There are some CMake options you might care about:
| flag | default | does |
| ---------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ENABLE_IO_THREAD` | `ON` | When enabled, we start up a thread to do io processing, if disabled you should call `Discord_UpdateConnection` yourself. |
| `USE_STATIC_CRT` | `OFF` | (Windows) Enable to statically link the CRT, avoiding requiring users install the redistributable package. (The prebuilt binaries enable this option) |
| [`BUILD_SHARED_LIBS`](https://cmake.org/cmake/help/v3.7/variable/BUILD_SHARED_LIBS.html) | `OFF` | Build library as a DLL |
| `WARNINGS_AS_ERRORS` | `OFF` | When enabled, compiles with `-Werror` (on \*nix platforms). |
## Continuous Builds
Why do we have three of these? Three times the fun!
| CI | badge |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| TravisCI | [![Build status](https://travis-ci.org/discordapp/discord-rpc.svg?branch=master)](https://travis-ci.org/discordapp/discord-rpc) |
| AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/qvkoc0w1c4f4b8tj?svg=true)](https://ci.appveyor.com/project/crmarsh/discord-rpc) |
| Buildkite (internal) | [![Build status](https://badge.buildkite.com/e103d79d247f6776605a15246352a04b8fd83d69211b836111.svg)](https://buildkite.com/discord/discord-rpc) |
## Sample: send-presence
This is a text adventure "game" that inits/deinits the connection to Discord, and sends a presence update on each command.
## Sample: button-clicker
This is a sample [Unity](https://unity3d.com/) project that wraps a DLL version of the library, and sends presence updates when you click on a button. Run `python build.py unity` in the root directory to build the correct library files and place them in their respective folders.
## Sample: unrealstatus
This is a sample [Unreal](https://www.unrealengine.com) project that wraps the DLL version of the library with an Unreal plugin, exposes a blueprint class for interacting with it, and uses that to make a very simple UI. Run `python build.py unreal` in the root directory to build the correct library files and place them in their respective folders.
## Wrappers and Implementations
Below is a table of unofficial, community-developed wrappers for and implementations of Rich Presence in various languages. If you would like to have yours added, please make a pull request adding your repository to the table. The repository should include:
- The code
- A brief ReadMe of how to use it
- A working example
###### Rich Presence Wrappers and Implementations
| Name | Language |
| ------------------------------------------------------------------------- | --------------------------------- |
| [Discord RPC C#](https://github.com/Lachee/discord-rpc-csharp) | C# |
| [Discord RPC D](https://github.com/voidblaster/discord-rpc-d) | [D](https://dlang.org/) |
| [discord-rpc.jar](https://github.com/Vatuu/discord-rpc 'Discord-RPC.jar') | Java |
| [java-discord-rpc](https://github.com/MinnDevelopment/java-discord-rpc) | Java |
| [Discord-IPC](https://github.com/jagrosh/DiscordIPC) | Java |
| [Discord Rich Presence](https://npmjs.org/discord-rich-presence) | JavaScript |
| [drpc4k](https://github.com/Bluexin/drpc4k) | [Kotlin](https://kotlinlang.org/) |
| [lua-discordRPC](https://github.com/pfirsich/lua-discordRPC) | LuaJIT (FFI) |
| [pypresence](https://github.com/qwertyquerty/pypresence) | [Python](https://python.org/) |
| [SwordRPC](https://github.com/Azoy/SwordRPC) | [Swift](https://swift.org) |

View File

@@ -1,63 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_BACKOFF_H
#define DISCORD_BACKOFF_H
#include <algorithm>
#include <random>
#include <cstdint>
#include <ctime>
namespace discord_rpc {
struct Backoff {
int64_t minAmount;
int64_t maxAmount;
int64_t current;
int fails;
std::mt19937_64 randGenerator;
std::uniform_real_distribution<> randDistribution;
double rand01() { return randDistribution(randGenerator); }
Backoff(int64_t min, int64_t max)
: minAmount(min), maxAmount(max), current(min), fails(0), randGenerator(static_cast<uint64_t>(time(0))) {
}
void reset() {
fails = 0;
current = minAmount;
}
int64_t nextDelay() {
++fails;
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
current = std::min(current + delay, maxAmount);
return current;
}
};
} // namespace discord_rpc
#endif // DISCORD_BACKOFF_H

View File

@@ -1,48 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_CONNECTION_H
#define DISCORD_CONNECTION_H
// This is to wrap the platform specific kinds of connect/read/write.
#include <cstdlib>
namespace discord_rpc {
// not really connectiony, but need per-platform
int GetProcessId();
struct BaseConnection {
static BaseConnection *Create();
static void Destroy(BaseConnection*&);
bool isOpen = false;
bool Open();
bool Close();
bool Write(const void *data, size_t length);
bool Read(void *data, size_t length);
};
} // namespace discord_rpc
#endif // DISCORD_CONNECTION_H

View File

@@ -1,160 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_connection.h"
#include <cerrno>
#include <fcntl.h>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
namespace discord_rpc {
int GetProcessId() {
return ::getpid();
}
struct BaseConnectionUnix : public BaseConnection {
int sock{ -1 };
};
static BaseConnectionUnix Connection;
static sockaddr_un PipeAddr{};
#ifdef MSG_NOSIGNAL
static int MsgFlags = MSG_NOSIGNAL;
#else
static int MsgFlags = 0;
#endif
static const char *GetTempPath() {
const char *temp = getenv("XDG_RUNTIME_DIR");
temp = temp ? temp : getenv("TMPDIR");
temp = temp ? temp : getenv("TMP");
temp = temp ? temp : getenv("TEMP");
temp = temp ? temp : "/tmp";
return temp;
}
BaseConnection *BaseConnection::Create() {
PipeAddr.sun_family = AF_UNIX;
return &Connection;
}
void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
self->Close();
c = nullptr;
}
bool BaseConnection::Open() {
const char *tempPath = GetTempPath();
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (self->sock == -1) {
return false;
}
fcntl(self->sock, F_SETFL, O_NONBLOCK);
#ifdef SO_NOSIGPIPE
int optval = 1;
setsockopt(self->sock, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
#endif
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
snprintf(PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr));
if (err == 0) {
self->isOpen = true;
return true;
}
}
self->Close();
return false;
}
bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
if (self->sock == -1) {
return false;
}
close(self->sock);
self->sock = -1;
self->isOpen = false;
return true;
}
bool BaseConnection::Write(const void *data, size_t length) {
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
if (self->sock == -1) {
return false;
}
ssize_t sentBytes = send(self->sock, data, length, MsgFlags);
if (sentBytes < 0) {
Close();
}
return sentBytes == static_cast<ssize_t>(length);
}
bool BaseConnection::Read(void *data, size_t length) {
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
if (self->sock == -1) {
return false;
}
long res = recv(self->sock, data, length, MsgFlags);
if (res < 0) {
if (errno == EAGAIN) {
return false;
}
Close();
}
else if (res == 0) {
Close();
}
return static_cast<size_t>(res) == length;
}
} // namespace discord_rpc

View File

@@ -1,160 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_connection.h"
#define WIN32_LEAN_AND_MEAN
#define NOMCX
#define NOSERVICE
#define NOIME
#include <cassert>
#include <windows.h>
namespace discord_rpc {
int GetProcessId() {
return static_cast<int>(::GetCurrentProcessId());
}
struct BaseConnectionWin : public BaseConnection {
HANDLE pipe{ INVALID_HANDLE_VALUE };
};
static BaseConnectionWin Connection;
BaseConnection *BaseConnection::Create() {
return &Connection;
}
void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionWin*>(c);
self->Close();
c = nullptr;
}
bool BaseConnection::Open() {
wchar_t pipeName[]{ L"\\\\?\\pipe\\discord-ipc-0" };
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
pipeName[pipeDigit] = L'0';
auto self = reinterpret_cast<BaseConnectionWin*>(this);
for (;;) {
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (self->pipe != INVALID_HANDLE_VALUE) {
self->isOpen = true;
return true;
}
auto lastError = GetLastError();
if (lastError == ERROR_FILE_NOT_FOUND) {
if (pipeName[pipeDigit] < L'9') {
pipeName[pipeDigit]++;
continue;
}
}
else if (lastError == ERROR_PIPE_BUSY) {
if (!WaitNamedPipeW(pipeName, 10000)) {
return false;
}
continue;
}
return false;
}
}
bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionWin*>(this);
::CloseHandle(self->pipe);
self->pipe = INVALID_HANDLE_VALUE;
self->isOpen = false;
return true;
}
bool BaseConnection::Write(const void *data, size_t length) {
if (length == 0) {
return true;
}
auto self = reinterpret_cast<BaseConnectionWin*>(this);
assert(self);
if (!self) {
return false;
}
if (self->pipe == INVALID_HANDLE_VALUE) {
return false;
}
assert(data);
if (!data) {
return false;
}
const DWORD bytesLength = static_cast<DWORD>(length);
DWORD bytesWritten = 0;
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE && bytesWritten == bytesLength;
}
bool BaseConnection::Read(void *data, size_t length) {
assert(data);
if (!data) {
return false;
}
auto self = reinterpret_cast<BaseConnectionWin*>(this);
assert(self);
if (!self) {
return false;
}
if (self->pipe == INVALID_HANDLE_VALUE) {
return false;
}
DWORD bytesAvailable = 0;
if (::PeekNamedPipe(self->pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
if (bytesAvailable >= length) {
DWORD bytesToRead = static_cast<DWORD>(length);
DWORD bytesRead = 0;
if (::ReadFile(self->pipe, data, bytesToRead, &bytesRead, nullptr) == TRUE) {
assert(bytesToRead == bytesRead);
return true;
}
else {
Close();
}
}
}
else {
Close();
}
return false;
}
} // namespace discord_rpc

View File

@@ -1,64 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_MSG_QUEUE_H
#define DISCORD_MSG_QUEUE_H
#include <atomic>
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
// a consumer. Mutex up as needed.
namespace discord_rpc {
template<typename ElementType, std::size_t QueueSize>
class MsgQueue {
ElementType queue_[QueueSize];
std::atomic_uint nextAdd_{ 0 };
std::atomic_uint nextSend_{ 0 };
std::atomic_uint pendingSends_{ 0 };
public:
MsgQueue() {}
ElementType *GetNextAddMessage() {
// if we are falling behind, bail
if (pendingSends_.load() >= QueueSize) {
return nullptr;
}
auto index = (nextAdd_++) % QueueSize;
return &queue_[index];
}
void CommitAdd() { ++pendingSends_; }
bool HavePendingSends() const { return pendingSends_.load() != 0; }
ElementType *GetNextSendMessage() {
auto index = (nextSend_++) % QueueSize;
return &queue_[index];
}
void CommitSend() { --pendingSends_; }
};
} // namespace discord_rpc
#endif // DISCORD_MSG_QUEUE_H

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_REGISTER_H
#define DISCORD_REGISTER_H
#ifdef __cplusplus
extern "C" {
#endif
void Discord_Register(const char *applicationId, const char *command);
#ifdef __cplusplus
}
#endif
#endif // DISCORD_REGISTER_H

View File

@@ -1,120 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_rpc.h"
#include "discord_register.h"
#include <cstdio>
#include <errno.h>
#include <cstdlib>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
namespace {
static bool Mkdir(const char *path) {
int result = mkdir(path, 0755);
if (result == 0) {
return true;
}
if (errno == EEXIST) {
return true;
}
return false;
}
} // namespace
// We want to register games so we can run them from Discord client as discord-<appid>://
extern "C" void Discord_Register(const char *applicationId, const char *command) {
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
const char *home = getenv("HOME");
if (!home) {
return;
}
char exePath[1024]{};
if (!command || !command[0]) {
const ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
return;
}
exePath[size] = '\0';
command = exePath;
}
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
"Name=Game %s\n"
"Exec=%s %%u\n" // note: it really wants that %u in there
"Type=Application\n"
"NoDisplay=true\n"
"Categories=Discord;Games;\n"
"MimeType=x-scheme-handler/discord-%s;\n";
char desktopFile[2048]{};
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
if (fileLen <= 0) {
return;
}
char desktopFilename[256]{};
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
char desktopFilePath[1024]{};
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/share");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/applications");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, desktopFilename);
FILE *fp = fopen(desktopFilePath, "w");
if (fp) {
fwrite(desktopFile, 1, fileLen, fp);
fclose(fp);
}
else {
return;
}
char xdgMimeCommand[1024]{};
snprintf(xdgMimeCommand,
sizeof(xdgMimeCommand),
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
applicationId,
applicationId);
if (system(xdgMimeCommand) < 0) {
fprintf(stderr, "Failed to register mime handler\n");
}
}

View File

@@ -1,99 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include <stdio.h>
#include <sys/stat.h>
#import <AppKit/AppKit.h>
#include "discord_register.h"
static void RegisterCommand(const char *applicationId, const char *command) {
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
// the command therein (will pass to js's window.open, so requires a url-like thing)
// Note: will not work for sandboxed apps
NSString *home = NSHomeDirectory();
if (!home) {
return;
}
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
stringByAppendingPathComponent:@"Application Support"]
stringByAppendingPathComponent:@"discord"]
stringByAppendingPathComponent:@"games"]
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
stringByAppendingPathExtension:@"json"];
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
}
static void RegisterURL(const char *applicationId) {
char url[256];
snprintf(url, sizeof(url), "discord-%s", applicationId);
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
if (!myBundleId) {
fprintf(stderr, "No bundle id found\n");
return;
}
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
if (!myURL) {
fprintf(stderr, "No bundle url found\n");
return;
}
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
if (status != noErr) {
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
return;
}
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
if (status != noErr) {
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
}
}
void Discord_Register(const char *applicationId, const char *command) {
if (command) {
RegisterCommand(applicationId, command);
}
else {
// raii lite
@autoreleasepool {
RegisterURL(applicationId);
}
}
}

View File

@@ -1,165 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_rpc.h"
#include "discord_register.h"
#define WIN32_LEAN_AND_MEAN
#define NOMCX
#define NOSERVICE
#define NOIME
#include <windows.h>
#include <psapi.h>
#include <cstdio>
/**
* Updated fixes for MinGW and WinXP
* This block is written the way it does not involve changing the rest of the code
* Checked to be compiling
* 1) strsafe.h belongs to Windows SDK and cannot be added to MinGW
* #include guarded, functions redirected to <string.h> substitutes
* 2) RegSetKeyValueW and LSTATUS are not declared in <winreg.h>
* The entire function is rewritten
*/
#ifdef __MINGW32__
# include <wchar.h>
/// strsafe.h fixes
static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat, ...) {
HRESULT ret;
va_list va;
va_start(va, pszFormat);
cbDest /= 2; // Size is divided by 2 to convert from bytes to wide characters - causes segfault
// othervise
ret = vsnwprintf(pszDest, cbDest, pszFormat, va);
pszDest[cbDest - 1] = 0; // Terminate the string in case a buffer overflow; -1 will be returned
va_end(va);
return ret;
}
#else
# include <cwchar>
# include <strsafe.h>
#endif // __MINGW32__
/// winreg.h fixes
#ifndef LSTATUS
# define LSTATUS LONG
#endif
#ifdef RegSetKeyValueW
# undefine RegSetKeyValueW
#endif
#define RegSetKeyValueW regset
static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) {
HKEY htkey = hkey, hsubkey = nullptr;
LSTATUS ret;
if (subkey && subkey[0]) {
if ((ret = RegCreateKeyExW(hkey, subkey, 0, 0, 0, KEY_ALL_ACCESS, 0, &hsubkey, 0)) !=
ERROR_SUCCESS)
return ret;
htkey = hsubkey;
}
ret = RegSetValueExW(htkey, name, 0, type, static_cast<const BYTE*>(data), len);
if (hsubkey && hsubkey != hkey)
RegCloseKey(hsubkey);
return ret;
}
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
// we want to register games so we can run them as discord-<appid>://
// Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions.
wchar_t exeFilePath[MAX_PATH]{};
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
wchar_t openCommand[1024]{};
if (command && command[0]) {
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
}
else {
// StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath);
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
}
wchar_t protocolName[64]{};
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
wchar_t protocolDescription[128]{};
StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
wchar_t urlProtocol = 0;
wchar_t keyName[256]{};
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
HKEY key;
auto status = RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
if (status != ERROR_SUCCESS) {
fprintf(stderr, "Error creating key\n");
return;
}
DWORD len;
LSTATUS result;
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing description\n");
}
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
result = RegSetKeyValueW(key, nullptr, L"URL Protocol", REG_SZ, &urlProtocol, sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing description\n");
}
result = RegSetKeyValueW(key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing icon\n");
}
len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
result = RegSetKeyValueW(key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing command\n");
}
RegCloseKey(key);
}
extern "C" void Discord_Register(const char *applicationId, const char *command) {
wchar_t appId[32]{};
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
wchar_t openCommand[1024]{};
const wchar_t *wcommand = nullptr;
if (command && command[0]) {
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
MultiByteToWideChar(CP_UTF8, 0, command, -1, openCommand, commandBufferLen);
wcommand = openCommand;
}
Discord_RegisterW(appId, wcommand);
}

View File

@@ -1,510 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include <atomic>
#include <chrono>
#include <mutex>
#include <condition_variable>
#include <thread>
#include "discord_rpc.h"
#include "discord_backoff.h"
#include "discord_register.h"
#include "discord_msg_queue.h"
#include "discord_rpc_connection.h"
#include "discord_serialization.h"
using namespace discord_rpc;
static void Discord_UpdateConnection();
namespace {
constexpr size_t MaxMessageSize{ 16 * 1024 };
constexpr size_t MessageQueueSize{ 8 };
constexpr size_t JoinQueueSize{ 8 };
struct QueuedMessage {
size_t length;
char buffer[MaxMessageSize];
void Copy(const QueuedMessage &other) {
length = other.length;
if (length) {
memcpy(buffer, other.buffer, length);
}
}
};
struct User {
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
// terminator = 21
char userId[32];
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
// terminator = 129
char username[344];
// 4 decimal digits + 1 null terminator = 5
char discriminator[8];
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
char avatar[128];
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
};
static RpcConnection *Connection{ nullptr };
static DiscordEventHandlers QueuedHandlers{};
static DiscordEventHandlers Handlers{};
static std::atomic_bool WasJustConnected{ false };
static std::atomic_bool WasJustDisconnected{ false };
static std::atomic_bool GotErrorMessage{ false };
static std::atomic_bool WasJoinGame{ false };
static std::atomic_bool WasSpectateGame{ false };
static std::atomic_bool UpdatePresence{ false };
static char JoinGameSecret[256];
static char SpectateGameSecret[256];
static int LastErrorCode{ 0 };
static char LastErrorMessage[256];
static int LastDisconnectErrorCode{ 0 };
static char LastDisconnectErrorMessage[256];
static std::mutex PresenceMutex;
static std::mutex HandlerMutex;
static QueuedMessage QueuedPresence{};
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
static User connectedUser;
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
static Backoff ReconnectTimeMs(500, 60 * 1000);
static auto NextConnect = std::chrono::system_clock::now();
static int Pid{ 0 };
static int Nonce{ 1 };
class IoThreadHolder {
private:
std::atomic_bool keepRunning{ true };
std::mutex waitForIOMutex;
std::condition_variable waitForIOActivity;
std::thread ioThread;
public:
void Start() {
keepRunning.store(true);
ioThread = std::thread([&]() {
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
Discord_UpdateConnection();
while (keepRunning.load()) {
std::unique_lock<std::mutex> lock(waitForIOMutex);
waitForIOActivity.wait_for(lock, maxWait);
Discord_UpdateConnection();
}
});
}
void Notify() { waitForIOActivity.notify_all(); }
void Stop() {
keepRunning.exchange(false);
Notify();
if (ioThread.joinable()) {
ioThread.join();
}
}
~IoThreadHolder() { Stop(); }
};
static IoThreadHolder *IoThread{ nullptr };
static void UpdateReconnectTime() {
NextConnect = std::chrono::system_clock::now() + std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
}
static void SignalIOActivity() {
if (IoThread != nullptr) {
IoThread->Notify();
}
}
static bool RegisterForEvent(const char *evtName) {
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length = JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd();
SignalIOActivity();
return true;
}
return false;
}
static bool DeregisterForEvent(const char *evtName) {
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length = JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd();
SignalIOActivity();
return true;
}
return false;
}
} // namespace
static void Discord_UpdateConnection() {
if (!Connection) {
return;
}
if (!Connection->IsOpen()) {
if (std::chrono::system_clock::now() >= NextConnect) {
UpdateReconnectTime();
Connection->Open();
}
}
else {
// reads
for (;;) {
JsonDocument message;
if (!Connection->Read(message)) {
break;
}
const char *evtName = GetStrMember(&message, "evt");
const char *nonce = GetStrMember(&message, "nonce");
if (nonce) {
// in responses only -- should use to match up response when needed.
if (evtName && strcmp(evtName, "ERROR") == 0) {
auto data = GetObjMember(&message, "data");
LastErrorCode = GetIntMember(data, "code");
StringCopy(LastErrorMessage, GetStrMember(data, "message", ""));
GotErrorMessage.store(true);
}
}
else {
// should have evt == name of event, optional data
if (evtName == nullptr) {
continue;
}
auto data = GetObjMember(&message, "data");
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) {
auto secret = GetStrMember(data, "secret");
if (secret) {
StringCopy(JoinGameSecret, secret);
WasJoinGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) {
auto secret = GetStrMember(data, "secret");
if (secret) {
StringCopy(SpectateGameSecret, secret);
WasSpectateGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) {
auto user = GetObjMember(data, "user");
auto userId = GetStrMember(user, "id");
auto username = GetStrMember(user, "username");
auto avatar = GetStrMember(user, "avatar");
auto joinReq = JoinAskQueue.GetNextAddMessage();
if (userId && username && joinReq) {
StringCopy(joinReq->userId, userId);
StringCopy(joinReq->username, username);
auto discriminator = GetStrMember(user, "discriminator");
if (discriminator) {
StringCopy(joinReq->discriminator, discriminator);
}
if (avatar) {
StringCopy(joinReq->avatar, avatar);
}
else {
joinReq->avatar[0] = 0;
}
JoinAskQueue.CommitAdd();
}
}
}
}
// writes
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
QueuedMessage local;
{
std::lock_guard<std::mutex> guard(PresenceMutex);
local.Copy(QueuedPresence);
}
if (!Connection->Write(local.buffer, local.length)) {
// if we fail to send, requeue
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.Copy(local);
UpdatePresence.exchange(true);
}
}
while (SendQueue.HavePendingSends()) {
auto qmessage = SendQueue.GetNextSendMessage();
Connection->Write(qmessage->buffer, qmessage->length);
SendQueue.CommitSend();
}
}
}
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
IoThread = new (std::nothrow) IoThreadHolder();
if (IoThread == nullptr) {
return;
}
if (autoRegister) {
Discord_Register(applicationId, nullptr);
}
Pid = GetProcessId();
{
std::lock_guard<std::mutex> guard(HandlerMutex);
if (handlers) {
QueuedHandlers = *handlers;
}
else {
QueuedHandlers = {};
}
Handlers = {};
}
if (Connection) {
return;
}
Connection = RpcConnection::Create(applicationId);
Connection->onConnect = [](JsonDocument &readyMessage) {
Discord_UpdateHandlers(&QueuedHandlers);
if (QueuedPresence.length > 0) {
UpdatePresence.exchange(true);
SignalIOActivity();
}
auto data = GetObjMember(&readyMessage, "data");
auto user = GetObjMember(data, "user");
auto userId = GetStrMember(user, "id");
auto username = GetStrMember(user, "username");
auto avatar = GetStrMember(user, "avatar");
if (userId && username) {
StringCopy(connectedUser.userId, userId);
StringCopy(connectedUser.username, username);
auto discriminator = GetStrMember(user, "discriminator");
if (discriminator) {
StringCopy(connectedUser.discriminator, discriminator);
}
if (avatar) {
StringCopy(connectedUser.avatar, avatar);
}
else {
connectedUser.avatar[0] = 0;
}
}
WasJustConnected.exchange(true);
ReconnectTimeMs.reset();
};
Connection->onDisconnect = [](int err, const char *message) {
LastDisconnectErrorCode = err;
StringCopy(LastDisconnectErrorMessage, message);
WasJustDisconnected.exchange(true);
UpdateReconnectTime();
};
IoThread->Start();
}
extern "C" void Discord_Shutdown() {
if (!Connection) {
return;
}
Connection->onConnect = nullptr;
Connection->onDisconnect = nullptr;
Handlers = {};
QueuedPresence.length = 0;
UpdatePresence.exchange(false);
if (IoThread != nullptr) {
IoThread->Stop();
delete IoThread;
IoThread = nullptr;
}
RpcConnection::Destroy(Connection);
}
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
{
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
UpdatePresence.exchange(true);
}
SignalIOActivity();
}
extern "C" void Discord_ClearPresence(void) {
Discord_UpdatePresence(nullptr);
}
extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) {
// if we are not connected, let's not batch up stale messages for later
if (!Connection || !Connection->IsOpen()) {
return;
}
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
SendQueue.CommitAdd();
SignalIOActivity();
}
}
extern "C" void Discord_RunCallbacks() {
// Note on some weirdness: internally we might connect, get other signals, disconnect any number
// of times inbetween calls here. Externally, we want the sequence to seem sane, so any other
// signals are book-ended by calls to ready and disconnect.
if (!Connection) {
return;
}
const bool wasDisconnected = WasJustDisconnected.exchange(false);
const bool isConnected = Connection->IsOpen();
if (isConnected) {
// if we are connected, disconnect cb first
std::lock_guard<std::mutex> guard(HandlerMutex);
if (wasDisconnected && Handlers.disconnected) {
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
}
}
if (WasJustConnected.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.ready) {
DiscordUser du{ connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
Handlers.ready(&du);
}
}
if (GotErrorMessage.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.errored) {
Handlers.errored(LastErrorCode, LastErrorMessage);
}
}
if (WasJoinGame.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinGame) {
Handlers.joinGame(JoinGameSecret);
}
}
if (WasSpectateGame.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.spectateGame) {
Handlers.spectateGame(SpectateGameSecret);
}
}
// Right now this batches up any requests and sends them all in a burst; I could imagine a world
// where the implementer would rather sequentially accept/reject each one before the next invite
// is sent. I left it this way because I could also imagine wanting to process these all and
// maybe show them in one common dialog and/or start fetching the avatars in parallel, and if
// not it should be trivial for the implementer to make a queue themselves.
while (JoinAskQueue.HavePendingSends()) {
const auto req = JoinAskQueue.GetNextSendMessage();
{
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinRequest) {
DiscordUser du{ req->userId, req->username, req->discriminator, req->avatar };
Handlers.joinRequest(&du);
}
}
JoinAskQueue.CommitSend();
}
if (!isConnected) {
// if we are not connected, disconnect message last
std::lock_guard<std::mutex> guard(HandlerMutex);
if (wasDisconnected && Handlers.disconnected) {
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
}
}
}
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
if (newHandlers) {
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
if (!Handlers.handler_name && newHandlers->handler_name) { \
RegisterForEvent(event); \
} \
else if (Handlers.handler_name && !newHandlers->handler_name) { \
DeregisterForEvent(event); \
}
std::lock_guard<std::mutex> guard(HandlerMutex);
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
HANDLE_EVENT_REGISTRATION(spectateGame, "ACTIVITY_SPECTATE")
HANDLE_EVENT_REGISTRATION(joinRequest, "ACTIVITY_JOIN_REQUEST")
#undef HANDLE_EVENT_REGISTRATION
Handlers = *newHandlers;
}
else {
std::lock_guard<std::mutex> guard(HandlerMutex);
Handlers = {};
}
}

View File

@@ -1,94 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_RPC_H
#define DISCORD_RPC_H
#include <cstdint>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct DiscordRichPresence {
int type;
int status_display_type;
const char *name; /* max 128 bytes */
const char *state; /* max 128 bytes */
const char *details; /* max 128 bytes */
int64_t startTimestamp;
int64_t endTimestamp;
const char *largeImageKey; /* max 32 bytes */
const char *largeImageText; /* max 128 bytes */
const char *smallImageKey; /* max 32 bytes */
const char *smallImageText; /* max 128 bytes */
const char *partyId; /* max 128 bytes */
int partySize;
int partyMax;
int partyPrivacy;
const char *matchSecret; /* max 128 bytes */
const char *joinSecret; /* max 128 bytes */
const char *spectateSecret; /* max 128 bytes */
int8_t instance;
} DiscordRichPresence;
typedef struct DiscordUser {
const char *userId;
const char *username;
const char *discriminator;
const char *avatar;
} DiscordUser;
typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser *request);
void (*disconnected)(int errorCode, const char *message);
void (*errored)(int errorCode, const char *message);
void (*joinGame)(const char *joinSecret);
void (*spectateGame)(const char *spectateSecret);
void (*joinRequest)(const DiscordUser *request);
} DiscordEventHandlers;
#define DISCORD_REPLY_NO 0
#define DISCORD_REPLY_YES 1
#define DISCORD_REPLY_IGNORE 2
#define DISCORD_PARTY_PRIVATE 0
#define DISCORD_PARTY_PUBLIC 1
void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister);
void Discord_Shutdown(void);
// checks for incoming messages, dispatches callbacks
void Discord_RunCallbacks(void);
void Discord_UpdatePresence(const DiscordRichPresence *presence);
void Discord_ClearPresence(void);
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif // DISCORD_RPC_H

View File

@@ -1,168 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_rpc_connection.h"
#include "discord_serialization.h"
namespace discord_rpc {
static const int RpcVersion = 1;
static RpcConnection Instance;
RpcConnection *RpcConnection::Create(const char *applicationId) {
Instance.connection = BaseConnection::Create();
StringCopy(Instance.appId, applicationId);
return &Instance;
}
void RpcConnection::Destroy(RpcConnection *&c) {
c->Close();
BaseConnection::Destroy(c->connection);
c = nullptr;
}
void RpcConnection::Open() {
if (state == State::Connected) {
return;
}
if (state == State::Disconnected && !connection->Open()) {
return;
}
if (state == State::SentHandshake) {
JsonDocument message;
if (Read(message)) {
auto cmd = GetStrMember(&message, "cmd");
auto evt = GetStrMember(&message, "evt");
if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) {
state = State::Connected;
if (onConnect) {
onConnect(message);
}
}
}
}
else {
sendFrame.opcode = Opcode::Handshake;
sendFrame.length = static_cast<uint32_t>(JsonWriteHandshakeObj(sendFrame.message, sizeof(sendFrame.message), RpcVersion, appId));
if (connection->Write(&sendFrame, sizeof(MessageFrameHeader) + sendFrame.length)) {
state = State::SentHandshake;
}
else {
Close();
}
}
}
void RpcConnection::Close() {
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
onDisconnect(lastErrorCode, lastErrorMessage);
}
connection->Close();
state = State::Disconnected;
}
bool RpcConnection::Write(const void *data, size_t length) {
sendFrame.opcode = Opcode::Frame;
memcpy(sendFrame.message, data, length);
sendFrame.length = static_cast<uint32_t>(length);
if (!connection->Write(&sendFrame, sizeof(MessageFrameHeader) + length)) {
Close();
return false;
}
return true;
}
bool RpcConnection::Read(JsonDocument &message) {
if (state != State::Connected && state != State::SentHandshake) {
return false;
}
MessageFrame readFrame{};
for (;;) {
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
if (!didRead) {
if (!connection->isOpen) {
lastErrorCode = static_cast<int>(ErrorCode::PipeClosed);
StringCopy(lastErrorMessage, "Pipe closed");
Close();
}
return false;
}
if (readFrame.length > 0) {
didRead = connection->Read(readFrame.message, readFrame.length);
if (!didRead) {
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
StringCopy(lastErrorMessage, "Partial data in frame");
Close();
return false;
}
readFrame.message[readFrame.length] = 0;
}
switch (readFrame.opcode) {
case Opcode::Close: {
message.ParseInsitu(readFrame.message);
lastErrorCode = GetIntMember(&message, "code");
StringCopy(lastErrorMessage, GetStrMember(&message, "message", ""));
Close();
return false;
}
case Opcode::Frame:
message.ParseInsitu(readFrame.message);
return true;
case Opcode::Ping:
readFrame.opcode = Opcode::Pong;
if (!connection->Write(&readFrame, sizeof(MessageFrameHeader) + readFrame.length)) {
Close();
}
break;
case Opcode::Pong:
break;
case Opcode::Handshake:
default:
// something bad happened
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
StringCopy(lastErrorMessage, "Bad ipc frame");
Close();
return false;
}
}
}
} // namespace discord_rpc

View File

@@ -1,88 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_RPC_CONNECTION_H
#define DISCORD_RPC_CONNECTION_H
#include "discord_connection.h"
#include "discord_serialization.h"
namespace discord_rpc {
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much smaller.
constexpr size_t MaxRpcFrameSize = 64 * 1024;
struct RpcConnection {
enum class ErrorCode : int {
Success = 0,
PipeClosed = 1,
ReadCorrupt = 2,
};
enum class Opcode : uint32_t {
Handshake = 0,
Frame = 1,
Close = 2,
Ping = 3,
Pong = 4,
};
struct MessageFrameHeader {
Opcode opcode;
uint32_t length;
};
struct MessageFrame : public MessageFrameHeader {
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
};
enum class State : uint32_t {
Disconnected,
SentHandshake,
AwaitingResponse,
Connected,
};
BaseConnection *connection{ nullptr };
State state{ State::Disconnected };
void (*onConnect)(JsonDocument &message) { nullptr };
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
char appId[64]{};
int lastErrorCode{ 0 };
char lastErrorMessage[256]{};
RpcConnection::MessageFrame sendFrame;
static RpcConnection *Create(const char *applicationId);
static void Destroy(RpcConnection*&);
inline bool IsOpen() const { return state == State::Connected; }
void Open();
void Close();
bool Write(const void *data, size_t length);
bool Read(JsonDocument &message);
};
} // namespace discord_rpc
#endif // DISCORD_RPC_CONNECTION_H

View File

@@ -1,285 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_serialization.h"
#include "discord_connection.h"
#include "discord_rpc.h"
namespace discord_rpc {
template<typename T>
void NumberToString(char *dest, T number) {
if (!number) {
*dest++ = '0';
*dest++ = 0;
return;
}
if (number < 0) {
*dest++ = '-';
number = -number;
}
char temp[32];
int place = 0;
while (number) {
auto digit = number % 10;
number = number / 10;
temp[place++] = '0' + static_cast<char>(digit);
}
for (--place; place >= 0; --place) {
*dest++ = temp[place];
}
*dest = 0;
}
// it's ever so slightly faster to not have to strlen the key
template<typename T>
void WriteKey(JsonWriter &w, T &k) {
w.Key(k, sizeof(T) - 1);
}
struct WriteObject {
JsonWriter &writer;
WriteObject(JsonWriter &w)
: writer(w) {
writer.StartObject();
}
template<typename T>
WriteObject(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartObject();
}
~WriteObject() { writer.EndObject(); }
};
struct WriteArray {
JsonWriter &writer;
template<typename T>
WriteArray(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartArray();
}
~WriteArray() { writer.EndArray(); }
};
template<typename T>
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
if (value && value[0]) {
w.Key(k, sizeof(T) - 1);
w.String(value);
}
}
static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
WriteKey(writer, "nonce");
char nonceBuffer[32];
NumberToString(nonceBuffer, nonce);
writer.String(nonceBuffer);
}
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
JsonWriter writer(dest, maxLen);
{
WriteObject top(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("SET_ACTIVITY");
{
WriteObject args(writer, "args");
WriteKey(writer, "pid");
writer.Int(pid);
if (presence != nullptr) {
WriteObject activity(writer, "activity");
if (presence->type >= 0 && presence->type <= 5) {
WriteKey(writer, "type");
writer.Int(presence->type);
WriteKey(writer, "status_display_type");
writer.Int(presence->status_display_type);
}
WriteOptionalString(writer, "name", presence->name);
WriteOptionalString(writer, "state", presence->state);
WriteOptionalString(writer, "details", presence->details);
if (presence->startTimestamp || presence->endTimestamp) {
WriteObject timestamps(writer, "timestamps");
if (presence->startTimestamp) {
WriteKey(writer, "start");
writer.Int64(presence->startTimestamp);
}
if (presence->endTimestamp) {
WriteKey(writer, "end");
writer.Int64(presence->endTimestamp);
}
}
if ((presence->largeImageKey && presence->largeImageKey[0]) ||
(presence->largeImageText && presence->largeImageText[0]) ||
(presence->smallImageKey && presence->smallImageKey[0]) ||
(presence->smallImageText && presence->smallImageText[0])) {
WriteObject assets(writer, "assets");
WriteOptionalString(writer, "large_image", presence->largeImageKey);
WriteOptionalString(writer, "large_text", presence->largeImageText);
WriteOptionalString(writer, "small_image", presence->smallImageKey);
WriteOptionalString(writer, "small_text", presence->smallImageText);
}
if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
presence->partyMax || presence->partyPrivacy) {
WriteObject party(writer, "party");
WriteOptionalString(writer, "id", presence->partyId);
if (presence->partySize && presence->partyMax) {
WriteArray size(writer, "size");
writer.Int(presence->partySize);
writer.Int(presence->partyMax);
}
if (presence->partyPrivacy) {
WriteKey(writer, "privacy");
writer.Int(presence->partyPrivacy);
}
}
if ((presence->matchSecret && presence->matchSecret[0]) ||
(presence->joinSecret && presence->joinSecret[0]) ||
(presence->spectateSecret && presence->spectateSecret[0])) {
WriteObject secrets(writer, "secrets");
WriteOptionalString(writer, "match", presence->matchSecret);
WriteOptionalString(writer, "join", presence->joinSecret);
WriteOptionalString(writer, "spectate", presence->spectateSecret);
}
writer.Key("instance");
writer.Bool(presence->instance != 0);
}
}
}
return writer.Size();
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
WriteKey(writer, "v");
writer.Int(version);
WriteKey(writer, "client_id");
writer.String(applicationId);
}
return writer.Size();
}
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("SUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
}
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("UNSUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
}
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
WriteKey(writer, "cmd");
if (reply == DISCORD_REPLY_YES) {
writer.String("SEND_ACTIVITY_JOIN_INVITE");
}
else {
writer.String("CLOSE_ACTIVITY_JOIN_REQUEST");
}
WriteKey(writer, "args");
{
WriteObject args(writer);
WriteKey(writer, "user_id");
writer.String(userId);
}
JsonWriteNonce(writer, nonce);
}
return writer.Size();
}
} // namespace discord_rpc

View File

@@ -1,213 +0,0 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_SERIALIZATION_H
#define DISCORD_SERIALIZATION_H
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
struct DiscordRichPresence;
namespace discord_rpc {
// if only there was a standard library function for this
template<size_t Len>
inline size_t StringCopy(char (&dest)[Len], const char *src) {
if (!src || !Len) {
return 0;
}
size_t copied;
char *out = dest;
for (copied = 1; *src && copied < Len; ++copied) {
*out++ = *src++;
}
*out = 0;
return copied - 1;
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
// Commands
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce);
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need
// to supply some of your own allocators for stuff rather than use the defaults
class LinearAllocator {
public:
char *buffer_;
char *end_;
LinearAllocator() {
assert(0); // needed for some default case in rapidjson, should not use
}
LinearAllocator(char *buffer, size_t size)
: buffer_(buffer), end_(buffer + size) {
}
static const bool kNeedFree = false;
void *Malloc(size_t size) {
char *res = buffer_;
buffer_ += size;
if (buffer_ > end_) {
buffer_ = res;
return nullptr;
}
return res;
}
void *Realloc(void *originalPtr, size_t originalSize, size_t newSize) {
if (newSize == 0) {
return nullptr;
}
// allocate how much you need in the first place
assert(!originalPtr && !originalSize);
// unused parameter warning
(void)(originalPtr);
(void)(originalSize);
return Malloc(newSize);
}
static void Free(void *ptr) {
/* shrug */
(void)ptr;
}
};
template<size_t Size>
class FixedLinearAllocator : public LinearAllocator {
public:
char fixedBuffer_[Size];
FixedLinearAllocator()
: LinearAllocator(fixedBuffer_, Size) {
}
static const bool kNeedFree = false;
};
// wonder why this isn't a thing already, maybe I missed it
class DirectStringBuffer {
public:
using Ch = char;
char *buffer_;
char *end_;
char *current_;
DirectStringBuffer(char *buffer, size_t maxLen)
: buffer_(buffer), end_(buffer + maxLen), current_(buffer) {
}
void Put(char c) {
if (current_ < end_) {
*current_++ = c;
}
}
void Flush() {}
size_t GetSize() const { return static_cast<size_t>(current_ - buffer_); }
};
using MallocAllocator = rapidjson::CrtAllocator;
using PoolAllocator = rapidjson::MemoryPoolAllocator<MallocAllocator>;
using UTF8 = rapidjson::UTF8<char>;
// Writer appears to need about 16 bytes per nested object level (with 64bit size_t)
using StackAllocator = FixedLinearAllocator<2048>;
constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t));
using JsonWriterBase =
rapidjson::Writer<DirectStringBuffer, UTF8, UTF8, StackAllocator, rapidjson::kWriteNoFlags>;
class JsonWriter : public JsonWriterBase {
public:
DirectStringBuffer stringBuffer_;
StackAllocator stackAlloc_;
JsonWriter(char *dest, size_t maxLen)
: JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels), stringBuffer_(dest, maxLen), stackAlloc_() {
}
size_t Size() const { return stringBuffer_.GetSize(); }
};
using JsonDocumentBase = rapidjson::GenericDocument<UTF8, PoolAllocator, StackAllocator>;
class JsonDocument : public JsonDocumentBase {
public:
static const int kDefaultChunkCapacity = 32 * 1024;
// json parser will use this buffer first, then allocate more if needed; I seriously doubt we
// send any messages that would use all of this, though.
char parseBuffer_[32 * 1024];
MallocAllocator mallocAllocator_;
PoolAllocator poolAllocator_;
StackAllocator stackAllocator_;
JsonDocument()
: JsonDocumentBase(rapidjson::kObjectType,
&poolAllocator_,
sizeof(stackAllocator_.fixedBuffer_),
&stackAllocator_),
poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_), stackAllocator_() {
}
};
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsObject()) {
return &member->value;
}
}
return nullptr;
}
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsInt()) {
return member->value.GetInt();
}
}
return notFoundDefault;
}
inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsString()) {
return member->value.GetString();
}
}
return notFoundDefault;
}
} // namespace discord_rpc
#endif // DISCORD_SERIALIZATION_H

View File

@@ -212,13 +212,24 @@ find_package(GTest)
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
find_package(RapidJSON)
find_package(discord-rpc)
if(TARGET discord-rpc::discord-rpc)
set(DISCORD_RPC_FOUND ON)
set(DISCORD_RPC_LIBRARIES "discord-rpc::discord-rpc")
else()
find_library(DISCORD_RPC_LIBRARY discord-rpc)
find_path(DISCORD_RPC_INCLUDE_DIRS NAMES discord-rpc.h)
if(DISCORD_RPC_LIBRARY)
set(DISCORD_RPC_FOUND ON)
set(DISCORD_RPC_LIBRARIES ${DISCORD_RPC_LIBRARY})
endif()
endif()
set(QT_VERSION_MAJOR 6)
set(QT_MIN_VERSION 6.4.0)
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
set(QT_COMPONENTS Core Concurrent Gui Widgets Network Sql)
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test Protobuf)
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test)
if(UNIX AND NOT APPLE)
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
endif()
@@ -278,7 +289,6 @@ if(APPLE OR WIN32)
if(TARGET "qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle")
set(QTSPARKLE_FOUND ON)
endif()
pkg_check_modules(TINYSVCMDNS IMPORTED_TARGET tinysvcmdns)
endif()
if(UNIX AND NOT APPLE)
@@ -379,21 +389,9 @@ optional_component(STREAMTAGREADER ON "Stream tagreader"
)
optional_component(DISCORD_RPC ON "Discord Rich Presence"
DEPENDS "RapidJSON" RapidJSON_FOUND
DEPENDS "discord-rpc" DISCORD_RPC_FOUND
)
if(WIN32)
optional_component(NETWORKREMOTE ON "Network remote"
DEPENDS "Qt Protobuf" Qt${QT_VERSION_MAJOR}Protobuf_FOUND
DEPENDS "tinysvcmdns" TINYSVCMDNS_FOUND
)
else()
optional_component(NETWORKREMOTE ON "Network remote"
DEPENDS "Qt Protobuf" Qt${QT_VERSION_MAJOR}Protobuf_FOUND
)
endif()
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
set(HAVE_CHROMAPRINT ON)
endif()
@@ -772,7 +770,6 @@ set(SOURCES
src/widgets/loginstatewidget.cpp
src/widgets/ratingwidget.cpp
src/widgets/resizabletextedit.cpp
src/widgets/filechooserwidget.cpp
src/osd/osdbase.cpp
src/osd/osdpretty.cpp
@@ -1071,7 +1068,6 @@ set(HEADERS
src/widgets/ratingwidget.h
src/widgets/forcescrollperpixel.h
src/widgets/resizabletextedit.h
src/widgets/filechooserwidget.h
src/osd/osdbase.h
src/osd/osdpretty.h
@@ -1495,57 +1491,6 @@ optional_source(HAVE_QOBUZ
src/settings/qobuzsettingspage.ui
)
if(HAVE_NETWORKREMOTE)
optional_source(HAVE_NETWORKREMOTE
SOURCES
src/core/zeroconf.cpp
src/networkremote/incomingdataparser.cpp
src/networkremote/networkremote.cpp
src/networkremote/outgoingdatacreator.cpp
src/networkremote/networkremoteclient.cpp
src/networkremote/songsender.cpp
src/settings/networkremotesettingspage.cpp
HEADERS
src/networkremote/networkremote.h
src/networkremote/incomingdataparser.h
src/networkremote/outgoingdatacreator.h
src/networkremote/networkremoteclient.h
src/networkremote/songsender.h
src/settings/networkremotesettingspage.h
UI
src/settings/networkremotesettingspage.ui
)
if(UNIX AND NOT APPLE)
get_target_property(QT_DBUSXML2CPP_EXECUTABLE Qt${QT_VERSION_MAJOR}::qdbusxml2cpp LOCATION)
add_custom_command(
OUTPUT
${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.cpp
${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.h
COMMAND ${QT_DBUSXML2CPP_EXECUTABLE}
${CMAKE_SOURCE_DIR}/src/avahi/org.freedesktop.Avahi.Server.xml
-p ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver
-i includes/dbus_metatypes.h
DEPENDS src/avahi/org.freedesktop.Avahi.Server.xml
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_custom_command(
OUTPUT
${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.cpp
${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.h
COMMAND ${QT_DBUSXML2CPP_EXECUTABLE}
${CMAKE_SOURCE_DIR}/src/avahi/org.freedesktop.Avahi.EntryGroup.xml
-p ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup
-i includes/dbus_metatypes.h
DEPENDS src/avahi/org.freedesktop.Avahi.EntryGroup.xml
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
list(APPEND SOURCES src/avahi/avahi.cpp ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.cpp ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.cpp)
list(APPEND HEADERS src/avahi/avahi.h ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.h ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.h)
endif()
optional_source(APPLE SOURCES src/core/bonjour.mm HEADERS src/core/bonjour.h)
optional_source(WIN32 SOURCES src/core/tinysvcmdns.cpp HEADERS src/core/tinysvcmdns.h)
endif()
qt_wrap_cpp(SOURCES ${HEADERS})
qt_wrap_ui(SOURCES ${UI})
qt_add_resources(SOURCES data/data.qrc data/icons.qrc)
@@ -1569,11 +1514,6 @@ if(LINUX AND LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
add_subdirectory(debian)
endif()
if(HAVE_DISCORD_RPC)
add_subdirectory(3rdparty/discord-rpc)
target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc)
endif()
if(HAVE_TRANSLATIONS)
qt_add_lupdate(strawberry_lib TS_FILES "${CMAKE_SOURCE_DIR}/src/translations/strawberry_en_US.ts" OPTIONS -locations none -no-ui-lines -no-obsolete)
file(GLOB_RECURSE ts_files ${CMAKE_SOURCE_DIR}/src/translations/*.ts)
@@ -1584,12 +1524,6 @@ if(HAVE_TRANSLATIONS)
endif()
endif()
if(HAVE_NETWORKREMOTE)
qt_add_protobuf(NetworkRemoteMessages
PROTO_FILES src/networkremote/networkremotemessages.proto
)
endif()
target_include_directories(strawberry_lib PUBLIC
${CMAKE_SOURCE_DIR}
${CMAKE_BINARY_DIR}
@@ -1601,6 +1535,10 @@ if(SINGLEAPPLICATION_INCLUDE_DIRS)
target_include_directories(strawberry_lib SYSTEM PUBLIC ${SINGLEAPPLICATION_INCLUDE_DIRS})
endif()
if(DISCORD_RPC_INCLUDE_DIRS)
target_include_directories(strawberry_lib SYSTEM PUBLIC ${DISCORD_RPC_INCLUDE_DIRS})
endif()
target_link_libraries(strawberry_lib PUBLIC
${CMAKE_THREAD_LIBS_INIT}
$<$<BOOL:${HAVE_BACKTRACE}>:${Backtrace_LIBRARIES}>
@@ -1622,7 +1560,6 @@ target_link_libraries(strawberry_lib PUBLIC
Qt${QT_VERSION_MAJOR}::Sql
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
$<$<BOOL:${HAVE_NETWORKREMOTE}>:Qt${QT_VERSION_MAJOR}::Protobuf>
ICU::uc
ICU::i18n
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>
@@ -1641,8 +1578,7 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
$<$<BOOL:${MSVC}>:WindowsApp>
KDAB::kdsingleapplication
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
$<$<BOOL:${HAVE_NETWORKREMOTE}>:NetworkRemoteMessages>
$<$<BOOL:${HAVE_DISCORD_RPC}>:${DISCORD_RPC_LIBRARIES}>
)
if(APPLE)
@@ -1661,10 +1597,6 @@ if(APPLE)
endif()
endif()
if(WIN32 AND HAVE_NETWORKREMOTE)
target_link_libraries(strawberry_lib PUBLIC PkgConfig::TINYSVCMDNS)
endif()
target_link_libraries(strawberry PUBLIC strawberry_lib)
if(NOT APPLE)

View File

@@ -1,101 +0,0 @@
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<?xml-stylesheet type="text/xsl" href="introspect.xsl"?>
<!DOCTYPE node SYSTEM "introspect.dtd">
<!--
This file is part of avahi.
avahi is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.
avahi is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with avahi; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.
-->
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" type="s" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.Avahi.EntryGroup">
<method name="Free"/>
<method name="Commit"/>
<method name="Reset"/>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="IsEmpty">
<arg name="empty" type="b" direction="out"/>
</method>
<method name="AddService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="host" type="s" direction="in"/>
<arg name="port" type="q" direction="in"/>
<arg name="txt" type="aay" direction="in"/>
</method>
<method name="AddServiceSubtype">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="subtype" type="s" direction="in"/>
</method>
<method name="UpdateServiceTxt">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="txt" type="aay" direction="in"/>
</method>
<method name="AddAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="address" type="s" direction="in"/>
</method>
<method name="AddRecord">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="ttl" type="u" direction="in"/>
<arg name="rdata" type="ay" direction="in"/>
</method>
</interface>
</node>

View File

@@ -1,405 +0,0 @@
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<?xml-stylesheet type="text/xsl" href="introspect.xsl"?>
<!DOCTYPE node SYSTEM "introspect.dtd">
<!--
This file is part of avahi.
avahi is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.
avahi is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with avahi; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.
-->
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" type="s" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.Avahi.Server">
<method name="GetVersionString">
<arg name="version" type="s" direction="out"/>
</method>
<method name="GetAPIVersion">
<arg name="version" type="u" direction="out"/>
</method>
<method name="GetHostName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="SetHostName">
<arg name="name" type="s" direction="in"/>
</method>
<method name="GetHostNameFqdn">
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetDomainName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="IsNSSSupportAvailable">
<arg name="yes" type="b" direction="out"/>
</method>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="GetLocalServiceCookie">
<arg name="cookie" type="u" direction="out"/>
</method>
<method name="GetAlternativeHostName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetAlternativeServiceName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceNameByIndex">
<arg name="index" type="i" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceIndexByName">
<arg name="name" type="s" direction="in"/>
<arg name="index" type="i" direction="out"/>
</method>
<method name="ResolveHostName">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="type" type="s" direction="out"/>
<arg name="domain" type="s" direction="out"/>
<arg name="host" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="port" type="q" direction="out"/>
<arg name="txt" type="aay" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="EntryGroupNew">
<arg name="path" type="o" direction="out"/>
</method>
<method name="DomainBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="btype" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceTypeBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="HostNameResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="AddressResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="RecordBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.Avahi.Server2">
<method name="GetVersionString">
<arg name="version" type="s" direction="out"/>
</method>
<method name="GetAPIVersion">
<arg name="version" type="u" direction="out"/>
</method>
<method name="GetHostName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="SetHostName">
<arg name="name" type="s" direction="in"/>
</method>
<method name="GetHostNameFqdn">
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetDomainName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="IsNSSSupportAvailable">
<arg name="yes" type="b" direction="out"/>
</method>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="GetLocalServiceCookie">
<arg name="cookie" type="u" direction="out"/>
</method>
<method name="GetAlternativeHostName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetAlternativeServiceName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceNameByIndex">
<arg name="index" type="i" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceIndexByName">
<arg name="name" type="s" direction="in"/>
<arg name="index" type="i" direction="out"/>
</method>
<method name="ResolveHostName">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="type" type="s" direction="out"/>
<arg name="domain" type="s" direction="out"/>
<arg name="host" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="port" type="q" direction="out"/>
<arg name="txt" type="aay" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="EntryGroupNew">
<arg name="path" type="o" direction="out"/>
</method>
<method name="DomainBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="btype" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceTypeBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="HostNameResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="AddressResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="RecordBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
</interface>
</node>

View File

@@ -1,114 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QByteArray>
#include <QString>
#include <QDBusConnection>
#include <QDBusPendingReply>
#include <QDBusPendingCallWatcher>
#include "core/logging.h"
#include "avahi.h"
#include "avahi/avahiserver.h"
#include "avahi/avahientrygroup.h"
using namespace Qt::StringLiterals;
Avahi::Avahi(QObject *parent) : Zeroconf(parent), port_(0), entry_group_interface_(nullptr) {}
void Avahi::PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port) {
domain_ = domain;
type_ = type;
name_ = name;
port_ = port;
OrgFreedesktopAvahiServerInterface server_interface(u"org.freedesktop.Avahi"_s, u"/"_s, QDBusConnection::systemBus());
QDBusPendingReply<QDBusObjectPath> reply = server_interface.EntryGroupNew();
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &Avahi::PublishInternalFinished);
}
void Avahi::PublishInternalFinished(QDBusPendingCallWatcher *watcher) {
const QDBusPendingReply<QDBusObjectPath> path_reply = watcher->reply();
watcher->deleteLater();
if (path_reply.isError()) {
qLog(Error) << "Failed to create Avahi entry group:" << path_reply.error();
qLog(Info) << "This might be because 'disable-user-service-publishing'" << "is set to 'yes' in avahi-daemon.conf";
return;
}
AddService(path_reply.reply().path());
}
void Avahi::AddService(const QString &path) {
entry_group_interface_ = new OrgFreedesktopAvahiEntryGroupInterface(u"org.freedesktop.Avahi"_s, path, QDBusConnection::systemBus());
QDBusPendingReply<> reply = entry_group_interface_->AddService(-1, -1, 0, QString::fromUtf8(name_.constData(), name_.size()), type_, domain_, QString(), port_);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &Avahi::AddServiceFinished);
}
void Avahi::AddServiceFinished(QDBusPendingCallWatcher *watcher) {
const QDBusPendingReply<QDBusObjectPath> path_reply = watcher->reply();
watcher->deleteLater();
if (path_reply.isError()) {
qLog(Error) << "Failed to add Avahi service:" << path_reply.error();
return;
}
Commit();
}
void Avahi::Commit() {
QDBusPendingReply<> reply = entry_group_interface_->Commit();
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &Avahi::CommitFinished);
}
void Avahi::CommitFinished(QDBusPendingCallWatcher *watcher) {
const QDBusPendingReply<QDBusObjectPath> path_reply = watcher->reply();
watcher->deleteLater();
entry_group_interface_->deleteLater();
entry_group_interface_ = nullptr;
if (path_reply.isError()) {
qLog(Debug) << "Commit error:" << path_reply.error();
}
else {
qLog(Debug) << "Remote interface published on Avahi";
}
}

View File

@@ -1,58 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef AVAHI_H
#define AVAHI_H
#include <QObject>
#include <QByteArray>
#include <QString>
#include "core/zeroconf.h"
class QDBusPendingCallWatcher;
class OrgFreedesktopAvahiEntryGroupInterface;
class Avahi : public Zeroconf {
Q_OBJECT
public:
explicit Avahi(QObject *parent = nullptr);
private:
void AddService(const QString &path);
void Commit();
private Q_SLOTS:
void PublishInternalFinished(QDBusPendingCallWatcher *watcher);
void AddServiceFinished(QDBusPendingCallWatcher *watcher);
void CommitFinished(QDBusPendingCallWatcher *watcher);
protected:
virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port) override;
private:
QString domain_;
QString type_;
QByteArray name_;
quint16 port_;
OrgFreedesktopAvahiEntryGroupInterface *entry_group_interface_;
};
#endif // AVAHI_H

View File

@@ -1,98 +0,0 @@
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE node SYSTEM "introspect.dtd">
<!--
This file is part of avahi.
avahi is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.
avahi is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with avahi; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.
-->
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" type="s" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.Avahi.EntryGroup">
<method name="Free"/>
<method name="Commit"/>
<method name="Reset"/>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="IsEmpty">
<arg name="empty" type="b" direction="out"/>
</method>
<method name="AddService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="host" type="s" direction="in"/>
<arg name="port" type="q" direction="in"/>
</method>
<method name="AddServiceSubtype">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="subtype" type="s" direction="in"/>
</method>
<method name="UpdateServiceTxt">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
</method>
<method name="AddAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="address" type="s" direction="in"/>
</method>
<method name="AddRecord">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="ttl" type="u" direction="in"/>
<arg name="rdata" type="ay" direction="in"/>
</method>
</interface>
</node>

View File

@@ -1,396 +0,0 @@
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE node SYSTEM "introspect.dtd">
<!--
This file is part of avahi.
avahi is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.
avahi is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with avahi; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.
-->
<node>
<interface name="org.freedesktop.Avahi.Server">
<method name="GetVersionString">
<arg name="version" type="s" direction="out"/>
</method>
<method name="GetAPIVersion">
<arg name="version" type="u" direction="out"/>
</method>
<method name="GetHostName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="SetHostName">
<arg name="name" type="s" direction="in"/>
</method>
<method name="GetHostNameFqdn">
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetDomainName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="IsNSSSupportAvailable">
<arg name="yes" type="b" direction="out"/>
</method>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="GetLocalServiceCookie">
<arg name="cookie" type="u" direction="out"/>
</method>
<method name="GetAlternativeHostName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetAlternativeServiceName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceNameByIndex">
<arg name="index" type="i" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceIndexByName">
<arg name="name" type="s" direction="in"/>
<arg name="index" type="i" direction="out"/>
</method>
<method name="ResolveHostName">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="type" type="s" direction="out"/>
<arg name="domain" type="s" direction="out"/>
<arg name="host" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="port" type="q" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="EntryGroupNew">
<arg name="path" type="o" direction="out"/>
</method>
<method name="DomainBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="btype" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceTypeBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="HostNameResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="AddressResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="RecordBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.Avahi.Server2">
<method name="GetVersionString">
<arg name="version" type="s" direction="out"/>
</method>
<method name="GetAPIVersion">
<arg name="version" type="u" direction="out"/>
</method>
<method name="GetHostName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="SetHostName">
<arg name="name" type="s" direction="in"/>
</method>
<method name="GetHostNameFqdn">
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetDomainName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="IsNSSSupportAvailable">
<arg name="yes" type="b" direction="out"/>
</method>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="GetLocalServiceCookie">
<arg name="cookie" type="u" direction="out"/>
</method>
<method name="GetAlternativeHostName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetAlternativeServiceName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceNameByIndex">
<arg name="index" type="i" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceIndexByName">
<arg name="name" type="s" direction="in"/>
<arg name="index" type="i" direction="out"/>
</method>
<method name="ResolveHostName">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="type" type="s" direction="out"/>
<arg name="domain" type="s" direction="out"/>
<arg name="host" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="port" type="q" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="EntryGroupNew">
<arg name="path" type="o" direction="out"/>
</method>
<method name="DomainBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="btype" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceTypeBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="HostNameResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="AddressResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="RecordBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
</interface>
</node>

View File

@@ -33,7 +33,6 @@
#cmakedefine HAVE_SPOTIFY
#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_DISCORD_RPC
#cmakedefine HAVE_NETWORKREMOTE
#cmakedefine HAVE_TAGLIB_DSFFILE
#cmakedefine HAVE_TAGLIB_DSDIFFFILE

View File

@@ -1,36 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef NETWORKREMOTECONSTANTS_H
#define NETWORKREMOTECONSTANTS_H
#include <QStringList>
using namespace Qt::Literals::StringLiterals;
namespace NetworkRemoteConstants {
const QStringList kDefaultMusicExtensionsAllowedRemotely = { u"aac"_s, u"alac"_s, u"flac"_s, u"m3u"_s, u"m4a"_s, u"mp3"_s, u"ogg"_s, u"wav"_s, u"wmv"_s };
constexpr quint16 kDefaultServerPort = 5500;
constexpr char kTranscoderSettingPostfix[] = "/NetworkRemote";
constexpr quint32 kFileChunkSize = 100000;
} // namespace NetworkRemoteConstants
#endif // NETWORKREMOTECONSTANTS_H

View File

@@ -1,35 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef NETWORKREMOTESETTINGSCONSTANTS_H
#define NETWORKREMOTESETTINGSCONSTANTS_H
namespace NetworkRemoteSettingsConstants {
constexpr char kSettingsGroup[] = "NetworkRemote";
constexpr char kEnabled[] = "enabled";
constexpr char kPort[] = "port";
constexpr char kAllowPublicAccess[] = "allow_public_access";
constexpr char kUseAuthCode[] = "use_authcode";
constexpr char kAuthCode[] = "authcode";
constexpr char kFilesRootFolder[] = "files_root_folder";
} // namespace NetworkRemoteSettingsConstants
#endif // NETWORKREMOTESETTINGSCONSTANTS_H

View File

@@ -110,10 +110,6 @@
# include "moodbar/moodbarloader.h"
#endif
#ifdef HAVE_NETWORKREMOTE
# include "networkremote/networkremote.h"
#endif
#include "radios/radioservices.h"
#include "radios/radiobackend.h"
@@ -220,13 +216,6 @@ class ApplicationImpl {
#ifdef HAVE_MOODBAR
moodbar_loader_([app]() { return new MoodbarLoader(app); }),
moodbar_controller_([app]() { return new MoodbarController(app->player(), app->moodbar_loader()); }),
#endif
#ifdef HAVE_NETWORKREMOTE
network_remote_([app]() {
NetworkRemote *networkremote = new NetworkRemote(app->database(), app->player(), app->collection_backend(), app->playlist_manager(), app->playlist_backend(), app->current_albumcover_loader(), app->scrobbler());
app->MoveToNewThread(networkremote);
return networkremote;
}),
#endif
lastfm_import_([app]() { return new LastFMImport(app->network()); })
{}
@@ -252,9 +241,6 @@ class ApplicationImpl {
#ifdef HAVE_MOODBAR
Lazy<MoodbarLoader> moodbar_loader_;
Lazy<MoodbarController> moodbar_controller_;
#endif
#ifdef HAVE_NETWORKREMOTE
Lazy<NetworkRemote> network_remote_;
#endif
Lazy<LastFMImport> lastfm_import_;
@@ -404,6 +390,3 @@ SharedPtr<LastFMImport> Application::lastfm_import() const { return p_->lastfm_i
SharedPtr<MoodbarController> Application::moodbar_controller() const { return p_->moodbar_controller_.ptr(); }
SharedPtr<MoodbarLoader> Application::moodbar_loader() const { return p_->moodbar_loader_.ptr(); }
#endif
#ifdef HAVE_NETWORKREMOTE
SharedPtr<NetworkRemote> Application::network_remote() const { return p_->network_remote_.ptr(); }
#endif

View File

@@ -63,9 +63,6 @@ class RadioServices;
class MoodbarController;
class MoodbarLoader;
#endif
#ifdef HAVE_NETWORKREMOTE
class NetworkRemote;
#endif
class Application : public QObject {
Q_OBJECT
@@ -106,10 +103,6 @@ class Application : public QObject {
SharedPtr<MoodbarLoader> moodbar_loader() const;
#endif
#ifdef HAVE_NETWORKREMOTE
SharedPtr<NetworkRemote> network_remote() const;
#endif
SharedPtr<LastFMImport> lastfm_import() const;
void Exit();

View File

@@ -1,24 +0,0 @@
#ifndef BONJOUR_H
#define BONJOUR_H
#include "zeroconf.h"
#ifdef __OBJC__
@class NetServicePublicationDelegate;
#else
class NetServicePublicationDelegate;
#endif // __OBJC__
class Bonjour : public Zeroconf {
public:
explicit Bonjour();
virtual ~Bonjour();
protected:
virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, const quint16 port);
private:
NetServicePublicationDelegate *delegate_;
};
#endif // BONJOUR_H

View File

@@ -1,57 +0,0 @@
#include "bonjour.h"
#import <Foundation/NSNetServices.h>
#import <Foundation/NSString.h>
#include "core/logging.h"
#include "core/scoped_nsautorelease_pool.h"
@interface NetServicePublicationDelegate : NSObject <NSNetServiceDelegate> {}
- (void)netServiceWillPublish:(NSNetService*)netService;
- (void)netService:(NSNetService*)netService didNotPublish:(NSDictionary*)errorDict;
- (void)netServiceDidStop:(NSNetService*)netService;
@end
@implementation NetServicePublicationDelegate
- (void)netServiceWillPublish:(NSNetService*)netService {
qLog(Debug) << "Publishing:" << [[netService name] UTF8String];
}
- (void)netService:(NSNetService*)netServie didNotPublish:(NSDictionary*)errorDict {
qLog(Debug) << "Failed to publish remote service with Bonjour";
NSLog(@"%@", errorDict);
}
- (void)netServiceDidStop:(NSNetService*)netService {
qLog(Debug) << "Unpublished:" << [[netService name] UTF8String];
}
@end
namespace {
NSString* NSStringFromQString(const QString& s) {
return [[NSString alloc] initWithUTF8String:s.toUtf8().constData()];
}
}
Bonjour::Bonjour() : delegate_([[NetServicePublicationDelegate alloc] init]) {}
Bonjour::~Bonjour() { [delegate_ release]; }
void Bonjour::PublishInternal(const QString& domain, const QString& type, const QByteArray& name, const quint16 port) {
ScopedNSAutoreleasePool pool;
NSNetService* service =
[[NSNetService alloc] initWithDomain:NSStringFromQString(domain)
type:NSStringFromQString(type)
name:[NSString stringWithUTF8String:name.constData()]
port:port];
if (service) {
[service setDelegate:delegate_];
[service publish];
}
}

View File

@@ -977,10 +977,6 @@ MainWindow::MainWindow(Application *app,
ui_->action_open_cd->setVisible(false);
#endif
#ifdef HAVE_NETWORKREMOTE
app_->network_remote();
#endif
// Load settings
qLog(Debug) << "Loading settings";
Settings settings;

View File

@@ -25,15 +25,14 @@
#include "mimedata.h"
MimeData::MimeData(const bool clear, const bool play_now, const bool enqueue, const bool enqueue_next_now, const bool open_in_new_playlist, const int playlist_id, QObject *parent)
MimeData::MimeData(const bool clear, const bool play_now, const bool enqueue, const bool enqueue_next_now, const bool open_in_new_playlist, QObject *parent)
: override_user_settings_(false),
clear_first_(clear),
play_now_(play_now),
enqueue_now_(enqueue),
enqueue_next_now_(enqueue_next_now),
open_in_new_playlist_(open_in_new_playlist),
from_doubleclick_(false),
playlist_id_(playlist_id) {
from_doubleclick_(false) {
Q_UNUSED(parent);

View File

@@ -29,7 +29,7 @@ class MimeData : public QMimeData {
Q_OBJECT
public:
explicit MimeData(const bool clear = false, const bool play_now = false, const bool enqueue = false, const bool enqueue_next_now = false, const bool open_in_new_playlist = false, const int playlist_id = -1, QObject *parent = nullptr);
explicit MimeData(const bool clear = false, const bool play_now = false, const bool enqueue = false, const bool enqueue_next_now = false, const bool open_in_new_playlist = false, QObject *parent = nullptr);
// If this is set then MainWindow will not touch any of the other flags.
bool override_user_settings_;
@@ -57,9 +57,6 @@ class MimeData : public QMimeData {
// The MainWindow will set the above flags to the defaults set by the user.
bool from_doubleclick_;
// The Network Remote can use this MimeData to drop songs on another playlist than the one currently opened on the server
int playlist_id_;
// Returns a pretty name for a playlist containing songs described by this MimeData object.
// By pretty name we mean the value of 'name_for_new_playlist_' or generic "Playlist" string if the 'name_for_new_playlist_' attribute is empty.
QString get_name_for_new_playlist() const;

View File

@@ -1,86 +0,0 @@
extern "C" {
#include "mdnsd.h"
}
#include <QObject>
#include <QList>
#include <QString>
#include <QHostInfo>
#include <QNetworkInterface>
#include <QtEndian>
#include "tinysvcmdns.h"
#include "core/logging.h"
using namespace Qt::Literals::StringLiterals;
TinySVCMDNS::TinySVCMDNS(QObject *parent) : Zeroconf(parent) {
// Get all network interfaces
const QList<QNetworkInterface> network_interfaces = QNetworkInterface::allInterfaces();
for (const QNetworkInterface &network_interface : network_interfaces) {
// Only use up and non loopback interfaces
if (network_interface.flags().testFlag(network_interface.IsUp) && !network_interface.flags().testFlag(network_interface.IsLoopBack)) {
qLog(Debug) << "Interface" << network_interface.humanReadableName();
uint32_t ipv4 = 0;
QString ipv6;
// Now check all network addresses for this device
QList<QNetworkAddressEntry> network_address_entries = network_interface.addressEntries();
for (QNetworkAddressEntry network_address_entry : network_address_entries) {
QHostAddress host_address = network_address_entry.ip();
if (host_address.protocol() == QAbstractSocket::IPv4Protocol) {
ipv4 = qToBigEndian(host_address.toIPv4Address());
qLog(Debug) << " ipv4:" << host_address.toString();
}
else if (host_address.protocol() == QAbstractSocket::IPv6Protocol) {
ipv6 = host_address.toString();
qLog(Debug) << " ipv6:" << host_address.toString();
}
}
// Now start the service
CreateMdnsd(ipv4, ipv6);
}
}
}
TinySVCMDNS::~TinySVCMDNS() {
for (mdnsd *mdnsd : std::as_const(mdnsd_)) {
mdnsd_stop(mdnsd);
}
}
void TinySVCMDNS::CreateMdnsd(const uint32_t ipv4, const QString &ipv6) {
const QString host = QHostInfo::localHostName();
// Start the service
mdnsd *mdnsd = mdnsd_start();
// Set our hostname
const QString fullhostname = host + ".local"_L1;
mdnsd_set_hostname(mdnsd, fullhostname.toUtf8().constData(), ipv4);
// Add to the list
mdnsd_.append(mdnsd);
}
void TinySVCMDNS::PublishInternal(const QString &domain, const QString &type, const QByteArray &name, const quint16 port) {
// Some pointless text, so tinymDNS publishes the service correctly.
const char *txt[] = { "cat=nyan", nullptr };
for (mdnsd *mdnsd : mdnsd_) {
const QString fulltype = type + ".local"_L1;
mdnsd_register_svc(mdnsd, name.constData(), fulltype.toUtf8().constData(), port, nullptr, txt);
}
}

View File

@@ -1,26 +0,0 @@
#ifndef TINYSVCMDNS_H
#define TINYSVCMDNS_H
#include <QList>
#include <QByteArray>
#include <QString>
#include "zeroconf.h"
struct mdnsd;
class TinySVCMDNS : public Zeroconf {
public:
explicit TinySVCMDNS(QObject *parent = nullptr);
virtual ~TinySVCMDNS();
protected:
virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, const quint16 port) override;
private:
void CreateMdnsd(const uint32_t ipv4, const QString &ipv6);
QList<mdnsd*> mdnsd_;
};
#endif // TINYSVCMDNS_H

View File

@@ -1,69 +0,0 @@
#include "config.h"
#include <QObject>
#include <QByteArray>
#include <QString>
#ifdef HAVE_DBUS
# include "avahi/avahi.h"
#endif
#ifdef Q_OS_DARWIN
# include "bonjour.h"
#endif
#ifdef Q_OS_WIN32
# include "tinysvcmdns.h"
#endif
#include "zeroconf.h"
Zeroconf *Zeroconf::sInstance = nullptr;
Zeroconf::Zeroconf(QObject *parent) : QObject(parent) {}
Zeroconf::~Zeroconf() = default;
Zeroconf *Zeroconf::GetZeroconf() {
if (!sInstance) {
#ifdef HAVE_DBUS
sInstance = new Avahi;
#endif // HAVE_DBUS
#ifdef Q_OS_DARWIN
sInstance = new Bonjour;
#endif
#ifdef Q_OS_WIN32
sInstance = new TinySVCMDNS;
#endif
}
return sInstance;
}
QByteArray Zeroconf::TruncateName(const QString &name) {
QByteArray truncated_utf8;
for (const QChar c : name) {
if (truncated_utf8.size() + 1 >= 63) {
break;
}
truncated_utf8 += c.toLatin1();
}
// NULL-terminate the string.
truncated_utf8.append('\0');
return truncated_utf8;
}
void Zeroconf::Publish(const QString &domain, const QString &type, const QString &name, quint16 port) {
const QByteArray truncated_name = TruncateName(name);
PublishInternal(domain, type, truncated_name, port);
}

View File

@@ -1,28 +0,0 @@
#ifndef ZEROCONF_H
#define ZEROCONF_H
#include <QObject>
#include <QByteArray>
#include <QString>
class Zeroconf : public QObject {
public:
explicit Zeroconf(QObject *parent);
virtual ~Zeroconf();
void Publish(const QString &domain, const QString &type, const QString &name, quint16 port);
static Zeroconf *GetZeroconf();
// Truncate a QString to 63 bytes of UTF-8.
static QByteArray TruncateName(const QString &name);
protected:
virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port) = 0;
private:
static Zeroconf *sInstance;
};
#endif // ZEROCONF_H

View File

@@ -75,7 +75,7 @@ void RichPresence::ReloadSettings() {
s.endGroup();
if (enabled && !initialized_) {
Discord_Initialize(kDiscordApplicationId, nullptr, 0);
Discord_Initialize(kDiscordApplicationId, nullptr, 0, nullptr);
initialized_ = true;
}
else if (!enabled && initialized_) {
@@ -124,7 +124,6 @@ void RichPresence::SendPresenceUpdate() {
// Listening to
presence_data.type = 2;
presence_data.status_display_type = status_display_type_;
presence_data.largeImageKey = kStrawberryIconResourceName;
presence_data.smallImageKey = kStrawberryIconResourceName;
presence_data.smallImageText = kStrawberryIconDescription;

View File

@@ -1,478 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <algorithm>
#include <QString>
#include <QUrl>
#include <QDir>
#include <QSettings>
#include "core/logging.h"
#include "core/mimedata.h"
#include "constants/timeconstants.h"
#include "engine/enginebase.h"
#include "playlist/playlist.h"
#include "playlist/playlistmanager.h"
#include "playlist/playlistsequence.h"
#include "incomingdataparser.h"
#include "scrobbler/audioscrobbler.h"
#include "constants/mainwindowsettings.h"
using namespace Qt::Literals::StringLiterals;
IncomingDataParser::IncomingDataParser(const SharedPtr<Player> player,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<AudioScrobbler> scrobbler,
QObject *parent)
: QObject(parent),
player_(player),
playlist_manager_(playlist_manager),
scrobbler_(scrobbler),
close_connection_(false),
doubleclick_playlist_addmode_(BehaviourSettings::PlaylistAddBehaviour::Enqueue) {
ReloadSettings();
QObject::connect(this, &IncomingDataParser::Play, &*player_, &Player::PlayHelper);
QObject::connect(this, &IncomingDataParser::PlayPause, &*player_, &Player::PlayPauseHelper);
QObject::connect(this, &IncomingDataParser::Pause, &*player_, &Player::Pause);
QObject::connect(this, &IncomingDataParser::Stop, &*player_, &Player::Stop);
QObject::connect(this, &IncomingDataParser::StopAfterCurrent, &*player_, &Player::StopAfterCurrent);
QObject::connect(this, &IncomingDataParser::Next, &*player_, &Player::Next);
QObject::connect(this, &IncomingDataParser::Previous, &*player_, &Player::Previous);
QObject::connect(this, &IncomingDataParser::SetVolume, &*player_, &Player::SetVolume);
QObject::connect(this, &IncomingDataParser::PlayAt, &*player_, &Player::PlayAt);
QObject::connect(this, &IncomingDataParser::SeekTo, &*player_, &Player::SeekTo);
QObject::connect(this, &IncomingDataParser::Enqueue, &*playlist_manager_, &PlaylistManager::Enqueue);
QObject::connect(this, &IncomingDataParser::SetActivePlaylist, &*playlist_manager_, &PlaylistManager::SetActivePlaylist);
QObject::connect(this, &IncomingDataParser::ShuffleCurrent, &*playlist_manager_, &PlaylistManager::ShuffleCurrent);
QObject::connect(this, &IncomingDataParser::InsertUrls, &*playlist_manager_, &PlaylistManager::InsertUrls);
QObject::connect(this, &IncomingDataParser::InsertSongs, &*playlist_manager_, &PlaylistManager::InsertSongs);
QObject::connect(this, &IncomingDataParser::RemoveSongs, &*playlist_manager_, &PlaylistManager::RemoveItemsWithoutUndo);
QObject::connect(this, &IncomingDataParser::New, &*playlist_manager_, &PlaylistManager::New);
QObject::connect(this, &IncomingDataParser::Open, &*playlist_manager_, &PlaylistManager::Open);
QObject::connect(this, &IncomingDataParser::Close, &*playlist_manager_, &PlaylistManager::Close);
QObject::connect(this, &IncomingDataParser::Clear, &*playlist_manager_, &PlaylistManager::Clear);
QObject::connect(this, &IncomingDataParser::Rename, &*playlist_manager_, &PlaylistManager::Rename);
QObject::connect(this, &IncomingDataParser::Favorite, &*playlist_manager_, &PlaylistManager::Favorite);
QObject::connect(this, &IncomingDataParser::SetRepeatMode, &*playlist_manager_->sequence(), &PlaylistSequence::SetRepeatMode);
QObject::connect(this, &IncomingDataParser::SetShuffleMode, &*playlist_manager_->sequence(), &PlaylistSequence::SetShuffleMode);
QObject::connect(this, &IncomingDataParser::RateCurrentSong, &*playlist_manager_, &PlaylistManager::RateCurrentSong);
QObject::connect(this, &IncomingDataParser::Love, &*scrobbler_, &AudioScrobbler::Love);
}
IncomingDataParser::~IncomingDataParser() = default;
void IncomingDataParser::ReloadSettings() {
QSettings s;
s.beginGroup(MainWindowSettings::kSettingsGroup);
doubleclick_playlist_addmode_ = static_cast<BehaviourSettings::PlaylistAddBehaviour>(s.value(BehaviourSettings::kDoubleClickPlaylistAddMode, static_cast<int>(BehaviourSettings::PlaylistAddBehaviour::Enqueue)).toInt());
s.endGroup();
}
bool IncomingDataParser::close_connection() const { return close_connection_; }
void IncomingDataParser::SetRemoteRootFiles(const QString &files_root_folder) {
files_root_folder_ = files_root_folder;
}
Song IncomingDataParser::SongFromPbSongMetadata(const networkremote::SongMetadata &pb_song_metadata) const {
Song song;
song.Init(pb_song_metadata.title(), pb_song_metadata.artist(), pb_song_metadata.album(), pb_song_metadata.length() * kNsecPerSec);
song.set_albumartist(pb_song_metadata.albumartist());
song.set_genre(pb_song_metadata.genre());
song.set_year(pb_song_metadata.prettyYear().toInt());
song.set_track(pb_song_metadata.track());
song.set_disc(pb_song_metadata.disc());
song.set_url(QUrl(pb_song_metadata.url()));
song.set_filesize(pb_song_metadata.fileSize());
song.set_rating(pb_song_metadata.rating());
song.set_basefilename(pb_song_metadata.filename());
song.set_art_automatic(QUrl(pb_song_metadata.artAutomatic()));
song.set_art_manual(QUrl(pb_song_metadata.artManual()));
song.set_filetype(static_cast<Song::FileType>(pb_song_metadata.filetype()));
return song;
}
void IncomingDataParser::Parse(const networkremote::Message &msg) {
close_connection_ = false;
NetworkRemoteClient *client = qobject_cast<NetworkRemoteClient*>(sender());
switch (msg.type()) {
case networkremote::MsgTypeGadget::MsgType::CONNECT:
ClientConnect(msg, client);
break;
case networkremote::MsgTypeGadget::MsgType::DISCONNECT:
close_connection_ = true;
break;
case networkremote::MsgTypeGadget::MsgType::GET_COLLECTION:
Q_EMIT SendCollection(client);
break;
case networkremote::MsgTypeGadget::MsgType::GET_PLAYLISTS:
ParseSendPlaylists(msg);
break;
case networkremote::MsgTypeGadget::MsgType::GET_PLAYLIST_SONGS:
ParseGetPlaylistSongs(msg);
break;
case networkremote::MsgTypeGadget::MsgType::SET_VOLUME:
Q_EMIT SetVolume(msg.requestSetVolume().volume());
break;
case networkremote::MsgTypeGadget::MsgType::PLAY:
Q_EMIT Play();
break;
case networkremote::MsgTypeGadget::MsgType::PLAYPAUSE:
Q_EMIT PlayPause();
break;
case networkremote::MsgTypeGadget::MsgType::PAUSE:
Q_EMIT Pause();
break;
case networkremote::MsgTypeGadget::MsgType::STOP:
Q_EMIT Stop();
break;
case networkremote::MsgTypeGadget::MsgType::STOP_AFTER:
Q_EMIT StopAfterCurrent();
break;
case networkremote::MsgTypeGadget::MsgType::NEXT:
Q_EMIT Next();
break;
case networkremote::MsgTypeGadget::MsgType::PREVIOUS:
Q_EMIT Previous();
break;
case networkremote::MsgTypeGadget::MsgType::CHANGE_SONG:
ParseChangeSong(msg);
break;
case networkremote::MsgTypeGadget::MsgType::SHUFFLE_PLAYLIST:
Q_EMIT ShuffleCurrent();
break;
case networkremote::MsgTypeGadget::MsgType::REPEAT:
ParseSetRepeatMode(msg.repeat());
break;
case networkremote::MsgTypeGadget::MsgType::SHUFFLE:
ParseSetShuffleMode(msg.shuffle());
break;
case networkremote::MsgTypeGadget::MsgType::SET_TRACK_POSITION:
Q_EMIT SeekTo(msg.requestSetTrackPosition().position());
break;
case networkremote::MsgTypeGadget::MsgType::PLAYLIST_INSERT_URLS:
ParseInsertUrls(msg);
break;
case networkremote::MsgTypeGadget::MsgType::REMOVE_PLAYLIST_SONGS:
ParseRemoveSongs(msg);
break;
case networkremote::MsgTypeGadget::MsgType::OPEN_PLAYLIST:
ParseOpenPlaylist(msg);
break;
case networkremote::MsgTypeGadget::MsgType::CLOSE_PLAYLIST:
ParseClosePlaylist(msg);
break;
case networkremote::MsgTypeGadget::MsgType::UPDATE_PLAYLIST:
ParseUpdatePlaylist(msg);
break;
case networkremote::MsgTypeGadget::MsgType::LOVE:
Q_EMIT Love();
break;
case networkremote::MsgTypeGadget::MsgType::GET_LYRICS:
Q_EMIT GetLyrics();
break;
case networkremote::MsgTypeGadget::MsgType::DOWNLOAD_SONGS:
client->song_sender()->SendSongs(msg.requestDownloadSongs());
break;
case networkremote::MsgTypeGadget::MsgType::SONG_OFFER_RESPONSE:
client->song_sender()->ResponseSongOffer(msg.responseSongOffer().accepted());
break;
case networkremote::MsgTypeGadget::MsgType::RATE_SONG:
ParseRateSong(msg);
break;
case networkremote::MsgTypeGadget::MsgType::REQUEST_FILES:
Q_EMIT SendListFiles(msg.requestListFiles().relativePath(), client);
break;
case networkremote::MsgTypeGadget::MsgType::APPEND_FILES:
ParseAppendFilesToPlaylist(msg);
break;
default:
break;
}
}
void IncomingDataParser::ClientConnect(const networkremote::Message &msg, NetworkRemoteClient *client) {
Q_EMIT SendInfo();
if (!client->isDownloader()) {
if (!msg.requestConnect().hasSendPlaylistSongs() || msg.requestConnect().sendPlaylistSongs()) {
Q_EMIT SendFirstData(true);
}
else {
Q_EMIT SendFirstData(false);
}
}
}
void IncomingDataParser::ParseGetPlaylistSongs(const networkremote::Message &msg) {
Q_EMIT SendPlaylistSongs(msg.requestPlaylistSongs().playlistId());
}
void IncomingDataParser::ParseChangeSong(const networkremote::Message &msg) {
// Get the first entry and check if there is a song
const networkremote::RequestChangeSong &request = msg.requestChangeSong();
// Check if we need to change the playlist
if (request.playlistId() != playlist_manager_->active_id()) {
Q_EMIT SetActivePlaylist(request.playlistId());
}
switch (doubleclick_playlist_addmode_) {
case BehaviourSettings::PlaylistAddBehaviour::Play:{
Q_EMIT PlayAt(request.songIndex(), false, 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Maybe, false, false);
break;
}
case BehaviourSettings::PlaylistAddBehaviour::Enqueue:{
Q_EMIT Enqueue(request.playlistId(), request.songIndex());
if (player_->GetState() != EngineBase::State::Playing) {
Q_EMIT PlayAt(request.songIndex(), false, 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Maybe, false, false);
}
break;
}
}
}
void IncomingDataParser::ParseSetRepeatMode(const networkremote::Repeat &repeat) {
switch (repeat.repeatMode()) {
case networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Off:
Q_EMIT SetRepeatMode(PlaylistSequence::RepeatMode::Off);
break;
case networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Track:
Q_EMIT SetRepeatMode(PlaylistSequence::RepeatMode::Track);
break;
case networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Album:
Q_EMIT SetRepeatMode(PlaylistSequence::RepeatMode::Album);
break;
case networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Playlist:
Q_EMIT SetRepeatMode(PlaylistSequence::RepeatMode::Playlist);
break;
default:
break;
}
}
void IncomingDataParser::ParseSetShuffleMode(const networkremote::Shuffle &shuffle) {
switch (shuffle.shuffleMode()) {
case networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_Off:
Q_EMIT SetShuffleMode(PlaylistSequence::ShuffleMode::Off);
break;
case networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_All:
Q_EMIT SetShuffleMode(PlaylistSequence::ShuffleMode::All);
break;
case networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_InsideAlbum:
Q_EMIT SetShuffleMode(PlaylistSequence::ShuffleMode::InsideAlbum);
break;
case networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_Albums:
Q_EMIT SetShuffleMode(PlaylistSequence::ShuffleMode::Albums);
break;
default:
break;
}
}
void IncomingDataParser::ParseInsertUrls(const networkremote::Message &msg) {
const networkremote::RequestInsertUrls &request = msg.requestInsertUrls();
int playlist_id = request.playlistId();
// Insert plain urls without metadata
if (!request.urls().empty()) {
QList<QUrl> urls;
for (auto it = request.urls().begin(); it != request.urls().end(); ++it) {
const QString s = *it;
urls << QUrl(s);
}
if (request.hasNewPlaylistName()) {
playlist_id = playlist_manager_->New(request.newPlaylistName());
}
// Insert the urls
Q_EMIT InsertUrls(playlist_id, urls, request.position(), request.playNow(), request.enqueue());
}
// Add songs with metadata if present
if (!request.songs().empty()) {
SongList songs;
for (int i = 0; i < request.songs().size(); i++) {
songs << SongFromPbSongMetadata(request.songs().at(i));
}
// Create a new playlist if required and not already done above by InsertUrls
if (request.hasNewPlaylistName() && playlist_id == request.playlistId()) {
playlist_id = playlist_manager_->New(request.newPlaylistName());
}
Q_EMIT InsertSongs(request.playlistId(), songs, request.position(), request.playNow(), request.enqueue());
}
}
void IncomingDataParser::ParseRemoveSongs(const networkremote::Message &msg) {
const networkremote::RequestRemoveSongs &request = msg.requestRemoveSongs();
QList<int> songs;
songs.reserve(request.songs().size());
for (int i = 0; i < request.songs().size(); i++) {
songs.append(request.songs().at(i));
}
Q_EMIT RemoveSongs(request.playlistId(), songs);
}
void IncomingDataParser::ParseSendPlaylists(const networkremote::Message &msg) {
if (!msg.hasRequestPlaylistSongs() || !msg.requestPlaylists().includeClosed()) {
Q_EMIT SendAllActivePlaylists();
}
else {
Q_EMIT SendAllPlaylists();
}
}
void IncomingDataParser::ParseOpenPlaylist(const networkremote::Message &msg) {
Q_EMIT Open(msg.requestOpenPlaylist().playlistId());
}
void IncomingDataParser::ParseClosePlaylist(const networkremote::Message &msg) {
Q_EMIT Close(msg.requestClosePlaylist().playlistId());
}
void IncomingDataParser::ParseUpdatePlaylist(const networkremote::Message &msg) {
const networkremote::RequestUpdatePlaylist &req_update = msg.requestUpdatePlaylist();
if (req_update.hasCreateNewPlaylist() && req_update.createNewPlaylist()) {
Q_EMIT New(req_update.hasNewPlaylistName() ? req_update.newPlaylistName() : u"New Playlist"_s);
return;
}
if (req_update.hasClearPlaylist() && req_update.clearPlaylist()) {
Q_EMIT Clear(req_update.playlistId());
return;
}
if (req_update.hasNewPlaylistName() && !req_update.newPlaylistName().isEmpty()) {
Q_EMIT Rename(req_update.playlistId(), req_update.newPlaylistName());
}
if (req_update.hasFavorite()) {
Q_EMIT Favorite(req_update.playlistId(), req_update.favorite());
}
}
void IncomingDataParser::ParseRateSong(const networkremote::Message &msg) {
Q_EMIT RateCurrentSong(msg.requestRateSong().rating());
}
void IncomingDataParser::ParseAppendFilesToPlaylist(const networkremote::Message &msg) {
if (files_root_folder_.isEmpty()) {
qLog(Warning) << "Remote root dir is not set although receiving APPEND_FILES request...";
return;
}
QDir root_dir(files_root_folder_);
if (!root_dir.exists()) {
qLog(Warning) << "Remote root dir doesn't exist...";
return;
}
const networkremote::RequestAppendFiles &req_append = msg.requestAppendFiles();
QString relative_path = req_append.relativePath();
if (relative_path.startsWith("/"_L1)) relative_path.remove(0, 1);
QFileInfo fi_folder(root_dir, relative_path);
if (!fi_folder.exists()) {
qLog(Warning) << "Remote relative path " << relative_path << " doesn't exist...";
return;
}
else if (!fi_folder.isDir()) {
qLog(Warning) << "Remote relative path " << relative_path << " is not a directory...";
return;
}
else if (root_dir.relativeFilePath(fi_folder.absoluteFilePath()).startsWith("../"_L1)) {
qLog(Warning) << "Remote relative path " << relative_path << " should not be accessed...";
return;
}
QList<QUrl> urls;
QDir dir(fi_folder.absoluteFilePath());
for (const auto &file : req_append.files()) {
QFileInfo fi(dir, file);
if (fi.exists()) urls << QUrl::fromLocalFile(fi.canonicalFilePath());
}
if (!urls.isEmpty()) {
MimeData *data = new MimeData;
data->setUrls(urls);
if (req_append.hasPlayNow()) {
data->play_now_ = req_append.playNow();
}
if (req_append.hasClearFirst()) {
data->clear_first_ = req_append.clearFirst();
}
if (req_append.hasNewPlaylistName()) {
QString playlist_name = req_append.newPlaylistName();
if (!playlist_name.isEmpty()) {
data->open_in_new_playlist_ = true;
data->name_for_new_playlist_ = playlist_name;
}
}
else if (req_append.hasPlaylistId()) {
// If playing we will drop the files in another playlist
if (player_->GetState() == EngineBase::State::Playing) {
data->playlist_id_ = req_append.playlistId();
}
else {
// As we may play the song, we change the current playlist
Q_EMIT SetCurrentPlaylist(req_append.playlistId());
}
}
Q_EMIT AddToPlaylistSignal(data);
}
}

View File

@@ -1,123 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef INCOMINGDATAPARSER_H
#define INCOMINGDATAPARSER_H
#include <QObject>
#include <QString>
#include <QStringList>
#include "constants/behavioursettings.h"
#include "core/player.h"
#include "networkremoteclient.h"
#include "networkremotemessages.qpb.h"
#include "playlist/playlistsequence.h"
class PlaylistManager;
class AudioScrobbler;
class IncomingDataParser : public QObject {
Q_OBJECT
public:
explicit IncomingDataParser(const SharedPtr<Player> player,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<AudioScrobbler> scrobbler,
QObject *parent = nullptr);
~IncomingDataParser();
bool close_connection() const;
void SetRemoteRootFiles(const QString &files_root_folder);
public Q_SLOTS:
void Parse(const networkremote::Message &msg);
void ReloadSettings();
Q_SIGNALS:
void SendInfo();
void SendFirstData(const bool send_playlist_songs);
void SendAllPlaylists();
void SendAllActivePlaylists();
void SendPlaylistSongs(const int id);
void New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString());
void Open(const int id);
void Clear(const int id);
void Close(const int id);
void Rename(const int id, const QString &new_playlist_name);
void Favorite(const int id, const bool favorite);
void GetLyrics();
void Love();
void Play();
void PlayPause();
void Pause();
void Stop(const bool stop_after = false);
void StopAfterCurrent();
void Next();
void Previous();
void SetVolume(const uint volume);
void PlayAt(const int index, const bool pause, const quint64 offset_nanosec, EngineBase::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const bool reshuffle, const bool force_inform);
void Enqueue(const int id, const int i);
void SetActivePlaylist(const int id);
void ShuffleCurrent();
void SetRepeatMode(const PlaylistSequence::RepeatMode repeat_mode);
void SetShuffleMode(const PlaylistSequence::ShuffleMode shuffle_mode);
void InsertUrls(const int id, const QList<QUrl> &urls, const int pos = -1, const bool play_now = false, const bool enqueue = false);
void InsertSongs(const int id, const SongList &songs, const int pos, const bool play_now, const bool enqueue);
void RemoveSongs(const int id, const QList<int> &indices);
void SeekTo(const quint64 seconds);
void SendCollection(NetworkRemoteClient *client);
void RateCurrentSong(const float rating);
void SendListFiles(const QString &path, NetworkRemoteClient *client);
void AddToPlaylistSignal(QMimeData *data);
void SetCurrentPlaylist(const int id);
private:
const SharedPtr<Player> player_;
const SharedPtr<PlaylistManager> playlist_manager_;
const SharedPtr<AudioScrobbler> scrobbler_;
bool close_connection_;
BehaviourSettings::PlaylistAddBehaviour doubleclick_playlist_addmode_;
QString files_root_folder_;
void ClientConnect(const networkremote::Message &msg, NetworkRemoteClient *client);
Song SongFromPbSongMetadata(const networkremote::SongMetadata &pb_song_metadata) const;
void ParseGetPlaylistSongs(const networkremote::Message &msg);
void ParseChangeSong(const networkremote::Message &msg);
void ParseSetRepeatMode(const networkremote::Repeat &repeat);
void ParseSetShuffleMode(const networkremote::Shuffle &shuffle);
void ParseInsertUrls(const networkremote::Message &msg);
void ParseRemoveSongs(const networkremote::Message &msg);
void ParseSendPlaylists(const networkremote::Message &msg);
void ParseOpenPlaylist(const networkremote::Message &msg);
void ParseClosePlaylist(const networkremote::Message &msg);
void ParseUpdatePlaylist(const networkremote::Message &msg);
void ParseRateSong(const networkremote::Message &msg);
void ParseAppendFilesToPlaylist(const networkremote::Message &msg);
};
#endif // INCOMINGDATAPARSER_H

View File

@@ -1,231 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <memory>
#include <QDataStream>
#include <QHostInfo>
#include <QNetworkProxy>
#include <QTcpSocket>
#include <QTcpServer>
#include <QSettings>
#include "constants/networkremotesettingsconstants.h"
#include "constants/networkremoteconstants.h"
#include "core/logging.h"
#include "core/zeroconf.h"
#include "playlist/playlistmanager.h"
#include "covermanager/currentalbumcoverloader.h"
#include "networkremote.h"
#include "incomingdataparser.h"
#include "outgoingdatacreator.h"
using namespace Qt::Literals::StringLiterals;
using namespace NetworkRemoteSettingsConstants;
using namespace NetworkRemoteConstants;
using std::make_unique;
NetworkRemote::NetworkRemote(const SharedPtr<Database> database,
const SharedPtr<Player> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<PlaylistBackend> playlist_backend,
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader,
const SharedPtr<AudioScrobbler> scrobbler,
QObject *parent)
: QObject(parent),
database_(database),
player_(player),
collection_backend_(collection_backend),
playlist_manager_(playlist_manager),
playlist_backend_(playlist_backend),
current_albumcover_loader_(current_albumcover_loader),
scrobbler_(scrobbler),
enabled_(false),
port_(0),
allow_public_access_(true),
signals_connected_(false) {
setObjectName("NetworkRemote");
ReloadSettings();
}
NetworkRemote::~NetworkRemote() { StopServer(); }
void NetworkRemote::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
enabled_ = s.value(kEnabled, false).toBool();
port_ = s.value("port", kDefaultServerPort).toInt();
allow_public_access_ = s.value(kAllowPublicAccess, false).toBool();
s.endGroup();
SetupServer();
StopServer();
StartServer();
}
void NetworkRemote::SetupServer() {
server_ = make_unique<QTcpServer>();
server_ipv6_ = make_unique<QTcpServer>();
incoming_data_parser_ = make_unique<IncomingDataParser>(player_, playlist_manager_, scrobbler_);
outgoing_data_creator_ = make_unique<OutgoingDataCreator>(database_, player_, playlist_manager_, playlist_backend_);
outgoing_data_creator_->SetClients(&clients_);
QObject::connect(&*current_albumcover_loader_, &CurrentAlbumCoverLoader::AlbumCoverLoaded, &*outgoing_data_creator_, &OutgoingDataCreator::CurrentSongChanged);
QObject::connect(&*server_, &QTcpServer::newConnection, this, &NetworkRemote::AcceptConnection);
QObject::connect(&*server_ipv6_, &QTcpServer::newConnection, this, &NetworkRemote::AcceptConnection);
QObject::connect(&*incoming_data_parser_, &IncomingDataParser::AddToPlaylistSignal, this, &NetworkRemote::AddToPlaylistSignal);
QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SetCurrentPlaylist, this, &NetworkRemote::SetCurrentPlaylist);
}
void NetworkRemote::StartServer() {
if (!enabled_) {
qLog(Info) << "Network Remote deactivated";
return;
}
qLog(Info) << "Starting network remote";
server_->setProxy(QNetworkProxy::NoProxy);
server_ipv6_->setProxy(QNetworkProxy::NoProxy);
server_->listen(QHostAddress::Any, port_);
server_ipv6_->listen(QHostAddress::AnyIPv6, port_);
qLog(Info) << "Listening on port " << port_;
if (Zeroconf::GetZeroconf()) {
QString name = QLatin1String("Strawberry on %1").arg(QHostInfo::localHostName());
Zeroconf::GetZeroconf()->Publish(u"local"_s, u"_strawberry._tcp"_s, name, port_);
}
}
void NetworkRemote::StopServer() {
if (server_->isListening()) {
outgoing_data_creator_->DisconnectAllClients();
server_->close();
server_ipv6_->close();
qDeleteAll(clients_);
clients_.clear();
}
}
void NetworkRemote::AcceptConnection() {
if (!signals_connected_) {
signals_connected_ = true;
QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendInfo, &*outgoing_data_creator_, &OutgoingDataCreator::SendInfo);
QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendFirstData, &*outgoing_data_creator_, &OutgoingDataCreator::SendFirstData);
QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendAllPlaylists, &*outgoing_data_creator_, &OutgoingDataCreator::SendAllPlaylists);
QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendAllActivePlaylists, &*outgoing_data_creator_, &OutgoingDataCreator::SendAllActivePlaylists);
QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendPlaylistSongs, &*outgoing_data_creator_, &OutgoingDataCreator::SendPlaylistSongs);
QObject::connect(&*playlist_manager_, &PlaylistManager::ActiveChanged, &*outgoing_data_creator_, &OutgoingDataCreator::ActiveChanged);
QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistChanged, &*outgoing_data_creator_, &OutgoingDataCreator::PlaylistChanged);
QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistAdded, &*outgoing_data_creator_, &OutgoingDataCreator::PlaylistAdded);
QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistRenamed, &*outgoing_data_creator_, &OutgoingDataCreator::PlaylistRenamed);
QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistClosed, &*outgoing_data_creator_, &OutgoingDataCreator::PlaylistClosed);
QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistDeleted, &*outgoing_data_creator_, &OutgoingDataCreator::PlaylistDeleted);
QObject::connect(&*player_, &Player::VolumeChanged, &*outgoing_data_creator_, &OutgoingDataCreator::VolumeChanged);
QObject::connect(&*player_->engine(), &EngineBase::StateChanged, &*outgoing_data_creator_, &OutgoingDataCreator::StateChanged);
QObject::connect(&*playlist_manager_->sequence(), &PlaylistSequence::RepeatModeChanged, &*outgoing_data_creator_, &OutgoingDataCreator::SendRepeatMode);
QObject::connect(&*playlist_manager_->sequence(), &PlaylistSequence::ShuffleModeChanged, &*outgoing_data_creator_, &OutgoingDataCreator::SendShuffleMode);
QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendCollection, &*outgoing_data_creator_, &OutgoingDataCreator::SendCollection);
QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendListFiles, &*outgoing_data_creator_, &OutgoingDataCreator::SendListFiles);
}
QTcpServer *server = qobject_cast<QTcpServer*>(sender());
QTcpSocket *client_socket = server->nextPendingConnection();
if (!allow_public_access_ && !IpIsPrivate(client_socket->peerAddress())) {
qLog(Warning) << "Got connection from public IP address" << client_socket->peerAddress().toString();
client_socket->close();
client_socket->deleteLater();
}
else {
CreateRemoteClient(client_socket);
}
}
bool NetworkRemote::IpIsPrivate(const QHostAddress &address) {
return
// Localhost v4
address.isInSubnet(QHostAddress::parseSubnet(u"127.0.0.0/8"_s)) ||
// Link Local v4
address.isInSubnet(QHostAddress::parseSubnet(u"169.254.1.0/16"_s)) ||
// Link Local v6
address.isInSubnet(QHostAddress::parseSubnet(u"::1/128"_s)) ||
address.isInSubnet(QHostAddress::parseSubnet(u"fe80::/10"_s)) ||
// Private v4 range
address.isInSubnet(QHostAddress::parseSubnet(u"192.168.0.0/16"_s)) ||
address.isInSubnet(QHostAddress::parseSubnet(u"172.16.0.0/12"_s)) ||
address.isInSubnet(QHostAddress::parseSubnet(u"10.0.0.0/8"_s)) ||
// Private v4 range translated to v6
address.isInSubnet(QHostAddress::parseSubnet(u"::ffff:192.168.0.0/112"_s)) ||
address.isInSubnet(QHostAddress::parseSubnet(u"::ffff:172.16.0.0/108"_s)) ||
address.isInSubnet(QHostAddress::parseSubnet(u"::ffff:10.0.0.0/104"_s)) ||
// Private v6 range
address.isInSubnet(QHostAddress::parseSubnet(u"fc00::/7"_s));
}
void NetworkRemote::CreateRemoteClient(QTcpSocket *client_socket) {
if (client_socket) {
NetworkRemoteClient *client = new NetworkRemoteClient(player_, collection_backend_, playlist_manager_, client_socket);
clients_.push_back(client);
// Update the Remote Root Files for the latest Client
outgoing_data_creator_->SetMusicExtensions(client->files_music_extensions());
outgoing_data_creator_->SetRemoteRootFiles(client->files_root_folder());
incoming_data_parser_->SetRemoteRootFiles(client->files_root_folder());
// Update OutgoingDataCreator with latest allow_downloads setting
outgoing_data_creator_->SetAllowDownloads(client->allow_downloads());
// Connect the signal to parse data
QObject::connect(client, &NetworkRemoteClient::Parse, &*incoming_data_parser_, &IncomingDataParser::Parse);
client->IncomingData();
}
}

View File

@@ -1,98 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef NETWORKREMOTE_H
#define NETWORKREMOTE_H
#include <QObject>
#include <QList>
#include "includes/shared_ptr.h"
#include "includes/scoped_ptr.h"
class QMimeData;
class QHostAddress;
class QTcpServer;
class QTcpSocket;
class Database;
class Player;
class CollectionBackend;
class PlaylistManager;
class PlaylistBackend;
class CurrentAlbumCoverLoader;
class AudioScrobbler;
class IncomingDataParser;
class OutgoingDataCreator;
class NetworkRemoteClient;
class NetworkRemote : public QObject {
Q_OBJECT
public:
explicit NetworkRemote(const SharedPtr<Database> database,
const SharedPtr<Player> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<PlaylistBackend> playlist_backend,
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader,
const SharedPtr<AudioScrobbler> scrobbler,
QObject *parent = nullptr);
~NetworkRemote();
Q_SIGNALS:
void AddToPlaylistSignal(QMimeData *data);
void SetCurrentPlaylist(const int id);
public Q_SLOTS:
void SetupServer();
void StartServer();
void ReloadSettings();
void AcceptConnection();
private:
const SharedPtr<Database> database_;
const SharedPtr<Player> player_;
const SharedPtr<CollectionBackend> collection_backend_;
const SharedPtr<PlaylistManager> playlist_manager_;
const SharedPtr<PlaylistBackend> playlist_backend_;
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader_;
const SharedPtr<AudioScrobbler> scrobbler_;
ScopedPtr<QTcpServer> server_;
ScopedPtr<QTcpServer> server_ipv6_;
ScopedPtr<IncomingDataParser> incoming_data_parser_;
ScopedPtr<OutgoingDataCreator> outgoing_data_creator_;
bool enabled_;
quint16 port_;
bool allow_public_access_;
bool signals_connected_;
QList<NetworkRemoteClient*> clients_;
void StopServer();
void CreateRemoteClient(QTcpSocket *client_socket);
bool IpIsPrivate(const QHostAddress &address);
};
#endif // NETWORKREMOTE_H

View File

@@ -1,223 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2013, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QDataStream>
#include <QTcpSocket>
#include <QProtobufSerializer>
#include <QSettings>
#include "constants/networkremotesettingsconstants.h"
#include "core/logging.h"
#include "networkremote.h"
#include "networkremoteclient.h"
#include "networkremotemessages.qpb.h"
using namespace Qt::Literals::StringLiterals;
using namespace NetworkRemoteSettingsConstants;
NetworkRemoteClient::NetworkRemoteClient(const SharedPtr<Player> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> playlist_manager,
QTcpSocket *socket,
QObject *parent)
: QObject(parent),
player_(player),
socket_(socket),
downloader_(false),
reading_protobuf_(false),
expected_length_(0),
song_sender_(new SongSender(player, collection_backend, playlist_manager, this)) {
QObject::connect(socket, &QTcpSocket::readyRead, this, &NetworkRemoteClient::ReadyRead);
QObject::connect(socket, &QTcpSocket::channelReadyRead, this, &NetworkRemoteClient::ReadyRead);
QSettings s;
s.beginGroup(kSettingsGroup);
use_auth_code_ = s.value(kUseAuthCode, false).toBool();
auth_code_ = s.value(kAuthCode, 0).toInt();
files_root_folder_ = s.value(kFilesRootFolder, ""_L1).toString();
s.endGroup();
authenticated_ = !use_auth_code_;
}
NetworkRemoteClient::~NetworkRemoteClient() {
socket_->close();
if (socket_->state() == QAbstractSocket::ConnectedState) {
socket_->waitForDisconnected(2000);
}
song_sender_->deleteLater();
socket_->deleteLater();
}
void NetworkRemoteClient::setDownloader(const bool downloader) { downloader_ = downloader; }
void NetworkRemoteClient::ReadyRead() {
IncomingData();
}
void NetworkRemoteClient::IncomingData() {
while (socket_->bytesAvailable()) {
if (!reading_protobuf_) {
// If we have less than 4 byte, we cannot read the length. Wait for more data
if (socket_->bytesAvailable() < 4) {
break;
}
// Read the length of the next message
QDataStream s(socket_);
s >> expected_length_;
// Receiving more than 128 MB is very unlikely
// Flush the data and disconnect the client
if (expected_length_ > 134217728) {
qLog(Debug) << "Received invalid data, disconnect client";
qLog(Debug) << "expected_length_ =" << expected_length_;
socket_->close();
return;
}
reading_protobuf_ = true;
}
// Read some of the message
buffer_.append(socket_->read(static_cast<qint32>(expected_length_) - buffer_.size()));
// Did we get everything?
if (buffer_.size() == static_cast<qint32>(expected_length_)) {
ParseMessage(buffer_);
// Clear the buffer
buffer_.clear();
reading_protobuf_ = false;
}
}
}
void NetworkRemoteClient::ParseMessage(const QByteArray &data) {
QProtobufSerializer serializer;
networkremote::Message msg;
if (!serializer.deserialize(&msg, data)) {
qLog(Info) << "Couldn't parse data:" << serializer.lastErrorString();
return;
}
if (msg.type() == networkremote::MsgTypeGadget::MsgType::CONNECT && use_auth_code_) {
if (msg.requestConnect().authCode() != auth_code_) {
DisconnectClient(networkremote::ReasonDisconnectGadget::ReasonDisconnect::Wrong_Auth_Code);
return;
}
else {
authenticated_ = true;
}
}
if (msg.type() == networkremote::MsgTypeGadget::MsgType::CONNECT) {
setDownloader(msg.requestConnect().hasDownloader() && msg.requestConnect().downloader());
qLog(Debug) << "Downloader" << downloader_;
}
// Check if downloads are allowed
if (msg.type() == networkremote::MsgTypeGadget::MsgType::DOWNLOAD_SONGS && !allow_downloads_) {
DisconnectClient(networkremote::ReasonDisconnectGadget::ReasonDisconnect::Download_Forbidden);
return;
}
if (msg.type() == networkremote::MsgTypeGadget::MsgType::DISCONNECT) {
socket_->abort();
qLog(Debug) << "Client disconnected";
return;
}
// Check if the client has sent the correct auth code
if (!authenticated_) {
DisconnectClient(networkremote::ReasonDisconnectGadget::ReasonDisconnect::Not_Authenticated);
return;
}
// Now parse the other data
Q_EMIT Parse(msg);
}
void NetworkRemoteClient::DisconnectClient(const networkremote::ReasonDisconnectGadget::ReasonDisconnect reason) {
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::DISCONNECT);
networkremote::ResponseDisconnect response_disconnect;
response_disconnect.setReasonDisconnect(reason);
msg.setResponseDisconnect(response_disconnect);
SendDataToClient(&msg);
// Just close the connection. The next time the outgoing data creator sends a keep alive, the client will be deleted
socket_->close();
}
// Sends data to client without check if authenticated
void NetworkRemoteClient::SendDataToClient(networkremote::Message *msg) {
//msg->setVersion(msg);
if (socket_->state() == QTcpSocket::ConnectedState) {
// Serialize the message
QProtobufSerializer serializer;
const QByteArray data = serializer.serialize(msg);
// Write the length of the data first
QDataStream s(socket_);
s << static_cast<qint32>(data.length());
if (downloader_) {
// Don't use QDataSteam for large files
socket_->write(data.data(), data.length());
}
else {
s.writeRawData(data.data(), data.length());
}
// Do NOT flush data here! If the client is already disconnected, it causes a SIGPIPE termination!!!
}
else {
qDebug() << "Closed";
socket_->close();
}
}
void NetworkRemoteClient::SendData(networkremote::Message *msg) {
if (authenticated_) {
SendDataToClient(msg);
}
}
QAbstractSocket::SocketState NetworkRemoteClient::State() const { return socket_->state(); }

View File

@@ -1,89 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2013, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef NETWORKREMOTECLIENT_H
#define NETWORKREMOTECLIENT_H
#include <QObject>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QAbstractSocket>
#include "networkremotemessages.qpb.h"
#include "songsender.h"
class QTcpSocket;
class NetworkRemoteClient : public QObject {
Q_OBJECT
public:
explicit NetworkRemoteClient(const SharedPtr<Player> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> playlist_manager,
QTcpSocket *client,
QObject *parent = nullptr);
~NetworkRemoteClient();
void SendData(networkremote::Message *msg);
QAbstractSocket::SocketState State() const;
void setDownloader(const bool downloader);
bool isDownloader() const { return downloader_; }
void DisconnectClient(const networkremote::ReasonDisconnectGadget::ReasonDisconnect reason);
SongSender *song_sender() const { return song_sender_; }
const QString &files_root_folder() const { return files_root_folder_; }
const QStringList &files_music_extensions() const { return files_music_extensions_; }
bool allow_downloads() const { return allow_downloads_; }
public Q_SLOTS:
void ReadyRead();
void IncomingData();
Q_SIGNALS:
void Parse(const networkremote::Message &msg);
private:
void ParseMessage(const QByteArray &data);
void SendDataToClient(networkremote::Message *msg);
private:
const SharedPtr<Player> player_;
QTcpSocket *socket_;
bool use_auth_code_;
int auth_code_;
bool authenticated_;
bool allow_downloads_;
bool downloader_;
bool reading_protobuf_;
quint32 expected_length_;
QByteArray buffer_;
SongSender *song_sender_;
QString files_root_folder_;
QStringList files_music_extensions_;
};
#endif // NETWORKREMOTECLIENT_H

View File

@@ -1,422 +0,0 @@
syntax = "proto2";
package networkremote;
enum MsgType {
UNKNOWN = 0;
CONNECT = 1;
DISCONNECT = 2;
INFO = 3;
KEEP_ALIVE = 4;
GET_COLLECTION = 5;
GET_PLAYLISTS = 6;
GET_PLAYLIST_SONGS = 7;
SEND_PLAYLISTS = 8;
SEND_PLAYLIST_SONGS = 10;
OPEN_PLAYLIST = 11;
CLOSE_PLAYLIST = 12;
UPDATE_PLAYLIST = 13;
REMOVE_PLAYLIST_SONGS = 14;
PLAYLIST_INSERT_URLS = 15;
CHANGE_SONG = 21;
SET_VOLUME = 22;
SET_TRACK_POSITION = 23;
GET_LYRICS = 24;
DOWNLOAD_SONGS = 25;
SONG_OFFER_RESPONSE = 26;
SONG_OFFER_FILE_CHUNK = 27;
CURRENT_METAINFO = 28;
ENGINE_STATE_CHANGED = 29;
UPDATE_TRACK_POSITION = 30;
ACTIVE_PLAYLIST_CHANGED = 31;
FIRST_DATA_SENT_COMPLETE = 32;
LYRICS = 33;
DOWNLOAD_QUEUE_EMPTY = 34;
COLLECTION_CHUNK = 35;
DOWNLOAD_TOTAL_SIZE = 36;
TRANSCODING_FILES = 37;
PLAYPAUSE = 101;
PLAY = 102;
PAUSE = 103;
STOP = 104;
STOP_AFTER = 105;
NEXT = 106;
PREVIOUS = 107;
SHUFFLE_PLAYLIST = 108;
REPEAT = 111;
SHUFFLE = 112;
LIST_FILES = 121;
REQUEST_FILES = 122;
APPEND_FILES = 123;
LOVE = 131;
RATE_SONG = 132;
}
enum EngineState {
EngineState_Empty = 0;
EngineState_Idle = 1;
EngineState_Playing = 2;
EngineState_Paused = 3;
}
message SongMetadata {
enum Source {
Source_Unknown = 0;
Source_LocalFile = 1;
Source_Collection = 2;
Source_CDDA = 3;
Source_Device = 4;
Source_Stream = 5;
Source_Tidal = 6;
Source_Subsonic = 7;
Source_Qobuz = 8;
Source_SomaFM = 9;
Source_RadioParadise = 10;
Source_Spotify = 11;
}
enum FileType {
FileType_Unknown = 0;
FileType_WAV = 1;
FileType_FLAC = 2;
FileType_WavPack = 3;
FileType_OggFlac = 4;
FileType_OggVorbis = 5;
FileType_OggOpus = 6;
FileType_OggSpeex = 7;
FileType_MPEG = 8;
FileType_MP4 = 9;
FileType_ASF = 10;
FileType_AIFF = 11;
FileType_MPC = 12;
FileType_TrueAudio = 13;
FileType_DSF = 14;
FileType_DSDIFF = 15;
FileType_PCM = 16;
FileType_APE = 17;
FileType_MOD = 18;
FileType_S3M = 19;
FileType_XM = 20;
FileType_IT = 21;
FileType_SPC = 22;
FileType_VGM = 23;
FileType_CDDA = 90;
FileType_Stream = 91;
}
optional int32 song_id = 1;
optional int32 index = 2;
optional string title = 3;
optional string album = 4;
optional string artist = 5;
optional string albumartist = 6;
optional int32 track = 7;
optional int32 disc = 8;
optional string pretty_year = 9;
optional string genre = 10;
optional uint32 playcount = 11;
optional string pretty_length = 12;
optional bytes art = 13;
optional int64 length = 14;
optional bool is_local = 15;
optional Source source = 22;
optional FileType filetype = 23;
optional string filename = 16;
optional int64 file_size = 17;
optional float rating = 18;
optional string url = 19;
optional string art_automatic = 20;
optional string art_manual = 21;
}
message Playlist {
optional int32 playlist_id = 1;
optional string name = 2;
optional int32 item_count = 3;
optional bool active = 4;
optional bool closed = 5;
optional bool favorite = 6;
}
enum RepeatMode {
RepeatMode_Off = 0;
RepeatMode_Track = 1;
RepeatMode_Album = 2;
RepeatMode_Playlist = 3;
RepeatMode_OneByOne = 4;
RepeatMode_Intro = 5;
}
enum ShuffleMode {
ShuffleMode_Off = 0;
ShuffleMode_All = 1;
ShuffleMode_InsideAlbum = 2;
ShuffleMode_Albums = 3;
}
message RequestPlaylists {
optional bool include_closed = 1;
}
message RequestPlaylistSongs {
optional int32 playlist_id = 1;
}
message RequestChangeSong {
optional int32 playlist_id = 1;
optional int32 song_index = 2;
}
message RequestSetVolume {
optional uint32 volume = 1;
}
message Repeat {
optional RepeatMode repeat_mode = 1;
}
message Shuffle {
optional ShuffleMode shuffle_mode = 1;
}
message ResponseInfo {
optional string version = 1;
optional EngineState state = 2;
optional bool allow_downloads = 3;
repeated string files_music_extensions = 4;
}
message ResponseCurrentMetadata {
optional SongMetadata song_metadata = 1;
}
message ResponsePlaylists {
repeated Playlist playlist = 1;
optional bool include_closed = 2;
}
message ResponsePlaylistSongs {
optional Playlist requested_playlist = 1;
repeated SongMetadata songs = 2;
}
message ResponseEngineStateChanged {
optional EngineState state = 1;
}
message ResponseUpdateTrackPosition {
optional int32 position = 1;
}
message RequestConnect {
optional int32 auth_code = 1;
optional bool send_playlist_songs = 2;
optional bool downloader = 3;
}
enum ReasonDisconnect {
Server_Shutdown = 1;
Wrong_Auth_Code = 2;
Not_Authenticated = 3;
Download_Forbidden = 4;
}
message ResponseDisconnect {
optional ReasonDisconnect reason_disconnect = 1;
}
message ResponseActiveChanged {
optional int32 playlist_id = 1;
}
message RequestSetTrackPosition {
optional int32 position = 1;
}
message RequestInsertUrls {
optional int32 playlist_id = 1;
repeated string urls = 2;
optional int32 position = 3 [default = -1];
optional bool play_now = 4 [default = false];
optional bool enqueue = 5 [default = false];
repeated SongMetadata songs = 6;
optional string new_playlist_name = 7;
}
message RequestRemoveSongs {
optional int32 playlist_id = 1;
repeated int32 songs = 2;
}
message RequestOpenPlaylist {
optional int32 playlist_id = 1;
}
message RequestClosePlaylist {
optional int32 playlist_id = 1;
}
message RequestUpdatePlaylist {
optional int32 playlist_id = 1;
optional string new_playlist_name = 2;
optional bool favorite = 3;
optional bool create_new_playlist = 4;
optional bool clear_playlist = 5;
}
message ResponseLyrics {
repeated Lyric lyrics = 1;
}
message Lyric {
optional string song_id = 1;
optional string title = 2;
optional string content = 3;
}
enum DownloadItem {
CurrentItem = 1;
ItemAlbum = 2;
APlaylist = 3;
Urls = 4;
}
message RequestDownloadSongs {
optional DownloadItem download_item = 1;
optional int32 playlist_id = 2;
repeated string urls = 3;
repeated int32 songs_ids = 4;
optional string relative_path = 5;
}
message ResponseSongFileChunk {
optional int32 chunk_number = 1;
optional int32 chunk_count = 2;
optional int32 file_number = 3;
optional int32 file_count = 4;
optional SongMetadata song_metadata = 6;
optional bytes data = 7;
optional int64 size = 8;
optional bytes file_hash = 9;
}
message ResponseCollectionChunk {
optional int32 chunk_number = 1;
optional int32 chunk_count = 2;
optional bytes data = 3;
optional int64 size = 4;
optional bytes file_hash = 5;
}
message ResponseSongOffer {
optional bool accepted = 1;
}
message RequestRateSong {
optional float rating = 1;
}
message ResponseDownloadTotalSize {
optional int64 total_size = 1;
optional int64 file_count = 2;
}
message ResponseTranscoderStatus {
optional int32 processed = 1;
optional int32 total = 2;
}
message RequestListFiles {
optional string relative_path = 1;
}
message FileMetadata {
optional string filename = 1;
optional bool is_dir = 2;
}
message ResponseListFiles {
enum Error {
NONE = 0;
ROOT_DIR_NOT_SET = 1;
DIR_NOT_ACCESSIBLE = 2;
DIR_NOT_EXIST = 3;
UNKNOWN = 4;
}
optional string relative_path = 1;
repeated FileMetadata files = 2;
optional Error error = 3;
}
message RequestAppendFiles {
optional int32 playlist_id = 1;
optional string new_playlist_name = 2;
optional string relative_path = 3;
repeated string files = 4;
optional bool play_now = 5;
optional bool clear_first = 6;
}
message Stream {
optional string name = 1;
optional string url = 2;
optional string url_logo = 3;
}
message Message {
optional int32 version = 1 [default = 21];
optional MsgType type = 2
[default = UNKNOWN];
optional RequestConnect request_connect = 21;
optional RequestPlaylists request_playlists = 27;
optional RequestPlaylistSongs request_playlist_songs = 10;
optional RequestChangeSong request_change_song = 11;
optional RequestSetVolume request_set_volume = 12;
optional RequestSetTrackPosition request_set_track_position = 23;
optional RequestInsertUrls request_insert_urls = 25;
optional RequestRemoveSongs request_remove_songs = 26;
optional RequestOpenPlaylist request_open_playlist = 28;
optional RequestClosePlaylist request_close_playlist = 29;
optional RequestUpdatePlaylist request_update_playlist = 53;
optional RequestDownloadSongs request_download_songs = 31;
optional RequestRateSong request_rate_song = 35;
optional RequestListFiles request_list_files = 50;
optional RequestAppendFiles request_append_files = 51;
optional Repeat repeat = 13;
optional Shuffle shuffle = 14;
optional ResponseInfo response_info = 15;
optional ResponseCurrentMetadata response_current_metadata = 16;
optional ResponsePlaylists response_playlists = 17;
optional ResponsePlaylistSongs response_playlist_songs = 18;
optional ResponseEngineStateChanged response_engine_state_changed = 19;
optional ResponseUpdateTrackPosition response_update_track_position = 20;
optional ResponseDisconnect response_disconnect = 22;
optional ResponseActiveChanged response_active_changed = 24;
optional ResponseLyrics response_lyrics = 30;
optional ResponseSongFileChunk response_song_file_chunk = 32;
optional ResponseSongOffer response_song_offer = 33;
optional ResponseCollectionChunk response_collection_chunk = 34;
optional ResponseDownloadTotalSize response_download_total_size = 36;
optional ResponseTranscoderStatus response_transcoder_status = 39;
optional ResponseListFiles response_list_files = 52;
}

View File

@@ -1,638 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <cmath>
#include <QCoreApplication>
#include <QDir>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QBuffer>
#include <QTimer>
#include <QStandardPaths>
#include "includes/shared_ptr.h"
#include "constants/timeconstants.h"
#include "utilities/randutils.h"
#include "core/player.h"
#include "core/database.h"
#include "core/sqlquery.h"
#include "core/logging.h"
#include "utilities/cryptutils.h"
#include "collection/collectionbackend.h"
#include "playlist/playlistmanager.h"
#include "playlist/playlistbackend.h"
#include "networkremote.h"
#include "networkremoteclient.h"
#include "outgoingdatacreator.h"
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
namespace {
constexpr quint32 kFileChunkSize = 100000;
}
OutgoingDataCreator::OutgoingDataCreator(const SharedPtr<Database> database,
const SharedPtr<Player> player,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<PlaylistBackend> playlist_backend,
QObject *parent)
: QObject(parent),
database_(database),
player_(player),
playlist_manager_(playlist_manager),
playlist_backend_(playlist_backend),
keep_alive_timer_(new QTimer(this)),
keep_alive_timeout_(10000) {
QObject::connect(keep_alive_timer_, &QTimer::timeout, this, &OutgoingDataCreator::SendKeepAlive);
}
OutgoingDataCreator::~OutgoingDataCreator() = default;
void OutgoingDataCreator::SetClients(QList<NetworkRemoteClient*> *clients) {
clients_ = clients;
// After we got some clients, start the keep alive timer
// Default: every 10 seconds
keep_alive_timer_->start(keep_alive_timeout_);
// Create the song position timer
track_position_timer_ = new QTimer(this);
QObject::connect(track_position_timer_, &QTimer::timeout, this, &OutgoingDataCreator::UpdateTrackPosition);
}
void OutgoingDataCreator::SendDataToClients(networkremote::Message *msg) {
if (clients_->empty()) {
return;
}
for (NetworkRemoteClient *client : std::as_const(*clients_)) {
// Do not send data to downloaders
if (client->isDownloader()) {
if (client->State() != QTcpSocket::ConnectedState) {
clients_->removeAt(clients_->indexOf(client));
delete client;
}
continue;
}
// Check if the client is still active
if (client->State() == QTcpSocket::ConnectedState) {
client->SendData(msg);
}
else {
clients_->removeAt(clients_->indexOf(client));
delete client;
}
}
}
void OutgoingDataCreator::SendInfo() {
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::INFO);
networkremote::ResponseInfo info;
info.setVersion(QLatin1String("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()));
info.setFilesMusicExtensions(files_music_extensions_);
info.setAllowDownloads(allow_downloads_);
info.setState(GetEngineState());
msg.setResponseInfo(info);
SendDataToClients(&msg);
}
void OutgoingDataCreator::SendKeepAlive() {
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::KEEP_ALIVE);
SendDataToClients(&msg);
}
networkremote::EngineStateGadget::EngineState OutgoingDataCreator::GetEngineState() {
switch (player_->GetState()) {
case EngineBase::State::Idle:
return networkremote::EngineStateGadget::EngineState::EngineState_Idle;
break;
case EngineBase::State::Error:
case EngineBase::State::Empty:
return networkremote::EngineStateGadget::EngineState::EngineState_Empty;
break;
case EngineBase::State::Playing:
return networkremote::EngineStateGadget::EngineState::EngineState_Playing;
break;
case EngineBase::State::Paused:
return networkremote::EngineStateGadget::EngineState::EngineState_Paused;
break;
}
return networkremote::EngineStateGadget::EngineState::EngineState_Empty;
}
void OutgoingDataCreator::SendAllPlaylists() {
// Get all Playlists
const int active_playlist = playlist_manager_->active_id();
// Create message
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::SEND_PLAYLISTS);
networkremote::ResponsePlaylists playlists = msg.responsePlaylists();
playlists.setIncludeClosed(true);
// Get all playlists, even ones that are hidden in the UI.
const QList<PlaylistBackend::Playlist> all_playlists = playlist_backend_->GetAllPlaylists();
for (const PlaylistBackend::Playlist &p : all_playlists) {
const bool playlist_open = playlist_manager_->IsPlaylistOpen(p.id);
const int item_count = playlist_open ? playlist_manager_->playlist(p.id)->rowCount() : 0;
// Create a new playlist
networkremote::Playlist playlist;// = playlists.playlist();
playlist.setPlaylistId(p.id);
playlist.setName(p.name);
playlist.setActive((p.id == active_playlist));
playlist.setItemCount(item_count);
playlist.setClosed(!playlist_open);
playlist.setFavorite(p.favorite);
}
SendDataToClients(&msg);
}
void OutgoingDataCreator::SendAllActivePlaylists() {
const int active_playlist = playlist_manager_->active_id();
const QList<Playlist*> playlists = playlist_manager_->GetAllPlaylists();
QList<networkremote::Playlist> pb_playlists;
pb_playlists.reserve(playlists.count());
for (Playlist *p : playlists) {
networkremote::Playlist pb_playlist;
pb_playlist.setPlaylistId(p->id());
pb_playlist.setName(playlist_manager_->GetPlaylistName(p->id()));
pb_playlist.setActive(p->id() == active_playlist);
pb_playlist.setItemCount(p->rowCount());
pb_playlist.setClosed(false);
pb_playlist.setFavorite(p->is_favorite());
pb_playlists << pb_playlist;
}
networkremote::ResponsePlaylists response_playlists;
response_playlists.setPlaylist(pb_playlists);
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::SEND_PLAYLISTS);
msg.setResponsePlaylists(response_playlists);
SendDataToClients(&msg);
}
void OutgoingDataCreator::ActiveChanged(Playlist *playlist) {
SendPlaylistSongs(playlist->id());
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::ACTIVE_PLAYLIST_CHANGED);
networkremote::ResponseActiveChanged response_active_changed;
response_active_changed.setPlaylistId(playlist->id());
msg.setResponseActiveChanged(response_active_changed);
SendDataToClients(&msg);
}
void OutgoingDataCreator::PlaylistAdded(const int id, const QString &name, const bool favorite) {
Q_UNUSED(id)
Q_UNUSED(name)
Q_UNUSED(favorite)
SendAllActivePlaylists();
}
void OutgoingDataCreator::PlaylistDeleted(const int id) {
Q_UNUSED(id)
SendAllActivePlaylists();
}
void OutgoingDataCreator::PlaylistClosed(const int id) {
Q_UNUSED(id)
SendAllActivePlaylists();
}
void OutgoingDataCreator::PlaylistRenamed(const int id, const QString &new_name) {
Q_UNUSED(id)
Q_UNUSED(new_name)
SendAllActivePlaylists();
}
void OutgoingDataCreator::SendFirstData(const bool send_playlist_songs) {
CurrentSongChanged(current_song_, albumcoverloader_result_);
VolumeChanged(player_->GetVolume());
if (!track_position_timer_->isActive() && player_->engine()->state() == EngineBase::State::Playing) {
track_position_timer_->start(1s);
}
UpdateTrackPosition();
SendAllActivePlaylists();
if (send_playlist_songs) {
SendPlaylistSongs(playlist_manager_->active_id());
}
SendShuffleMode(playlist_manager_->sequence()->shuffle_mode());
SendRepeatMode(playlist_manager_->sequence()->repeat_mode());
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::FIRST_DATA_SENT_COMPLETE);
SendDataToClients(&msg);
}
void OutgoingDataCreator::CurrentSongChanged(const Song &song, const AlbumCoverLoaderResult &result) {
albumcoverloader_result_ = result;
current_song_ = song;
current_image_ = result.album_cover.image;
SendSongMetadata();
}
void OutgoingDataCreator::SendSongMetadata() {
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::CURRENT_METAINFO);
const networkremote::SongMetadata pb_song_metadata = PbSongMetadataFromSong(playlist_manager_->active()->current_row(), current_song_, current_image_);
networkremote::ResponseCurrentMetadata response_current_metadata;
response_current_metadata.setSongMetadata(pb_song_metadata);
msg.setResponseCurrentMetadata(response_current_metadata);
SendDataToClients(&msg);
}
networkremote::SongMetadata OutgoingDataCreator::PbSongMetadataFromSong(const int index, const Song &song, const QImage &image_cover_art) {
if (!song.is_valid()) {
return networkremote::SongMetadata();
}
networkremote::SongMetadata pb_song_metadata;
pb_song_metadata.setSongId(song.id());
pb_song_metadata.setIndex(index);
pb_song_metadata.setTitle(song.PrettyTitle());
pb_song_metadata.setArtist(song.artist());
pb_song_metadata.setAlbum(song.album());
pb_song_metadata.setAlbumartist(song.albumartist());
pb_song_metadata.setLength(song.length_nanosec() / kNsecPerSec);
pb_song_metadata.setPrettyLength(song.PrettyLength());
pb_song_metadata.setGenre(song.genre());
pb_song_metadata.setPrettyYear(song.PrettyYear());
pb_song_metadata.setTrack(song.track());
pb_song_metadata.setDisc(song.disc());
pb_song_metadata.setPlaycount(song.playcount());
pb_song_metadata.setIsLocal(song.url().isLocalFile());
pb_song_metadata.setFilename(song.basefilename());
pb_song_metadata.setFileSize(song.filesize());
pb_song_metadata.setRating(song.rating());
pb_song_metadata.setUrl(song.url().toString());
pb_song_metadata.setArtAutomatic(song.art_automatic().toString());
pb_song_metadata.setArtManual(song.art_manual().toString());
pb_song_metadata.setFiletype(static_cast<networkremote::SongMetadata::FileType>(song.filetype()));
if (!image_cover_art.isNull()) {
QImage image_cover_art_small;
if (image_cover_art.width() > 1000 || image_cover_art.height() > 1000) {
image_cover_art_small = image_cover_art.scaled(1000, 1000, Qt::KeepAspectRatio);
}
else {
image_cover_art_small = image_cover_art;
}
QByteArray data;
QBuffer buffer(&data);
if (buffer.open(QIODevice::WriteOnly)) {
image_cover_art_small.save(&buffer, "JPG");
buffer.close();
}
pb_song_metadata.setArt(data);
}
return pb_song_metadata;
}
void OutgoingDataCreator::VolumeChanged(const uint volume) {
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::SET_VOLUME);
networkremote::RequestSetVolume request_set_volume;
request_set_volume.setVolume(volume);
msg.setRequestSetVolume(request_set_volume);
SendDataToClients(&msg);
}
void OutgoingDataCreator::SendPlaylistSongs(const int playlist_id) {
Playlist *playlist = playlist_manager_->playlist(playlist_id);
if (!playlist) {
qLog(Error) << "Could not find playlist with ID" << playlist_id;
return;
}
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::SEND_PLAYLIST_SONGS);
networkremote::Playlist pb_playlist;
pb_playlist.setPlaylistId(playlist_id);
networkremote::ResponsePlaylistSongs pb_response_playlist_songs;
pb_response_playlist_songs.setRequestedPlaylist(pb_playlist);
const SongList songs = playlist->GetAllSongs();
QList<networkremote::SongMetadata> pb_song_metadatas;
pb_song_metadatas.reserve(songs.count());
for (const Song &song : songs) {
pb_song_metadatas << PbSongMetadataFromSong(songs.indexOf(song), song);
}
pb_response_playlist_songs.setSongs(pb_song_metadatas);
msg.setResponsePlaylistSongs(pb_response_playlist_songs);
SendDataToClients(&msg);
}
void OutgoingDataCreator::PlaylistChanged(Playlist *playlist) {
SendPlaylistSongs(playlist->id());
}
void OutgoingDataCreator::StateChanged(const EngineBase::State state) {
if (state == last_state_) {
return;
}
last_state_ = state;
networkremote::Message msg;
switch (state) {
case EngineBase::State::Playing:
msg.setType(networkremote::MsgTypeGadget::MsgType::PLAY);
track_position_timer_->start(1s);
break;
case EngineBase::State::Paused:
msg.setType(networkremote::MsgTypeGadget::MsgType::PAUSE);
track_position_timer_->stop();
break;
case EngineBase::State::Empty:
msg.setType(networkremote::MsgTypeGadget::MsgType::STOP); // Empty is called when player stopped
track_position_timer_->stop();
break;
default:
msg.setType(networkremote::MsgTypeGadget::MsgType::STOP);
track_position_timer_->stop();
break;
};
SendDataToClients(&msg);
}
void OutgoingDataCreator::SendRepeatMode(const PlaylistSequence::RepeatMode mode) {
networkremote::Repeat repeat;
switch (mode) {
case PlaylistSequence::RepeatMode::Off:
repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Off);
break;
case PlaylistSequence::RepeatMode::Track:
repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Track);
break;
case PlaylistSequence::RepeatMode::Album:
repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Album);
break;
case PlaylistSequence::RepeatMode::Playlist:
repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Playlist);
break;
case PlaylistSequence::RepeatMode::OneByOne:
repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_OneByOne);
break;
case PlaylistSequence::RepeatMode::Intro:
repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Intro);
break;
}
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::REPEAT);
msg.setRepeat(repeat);
SendDataToClients(&msg);
}
void OutgoingDataCreator::SendShuffleMode(const PlaylistSequence::ShuffleMode mode) {
networkremote::Shuffle shuffle;
switch (mode) {
case PlaylistSequence::ShuffleMode::Off:
shuffle.setShuffleMode(networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_Off);
break;
case PlaylistSequence::ShuffleMode::All:
shuffle.setShuffleMode(networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_All);
break;
case PlaylistSequence::ShuffleMode::InsideAlbum:
shuffle.setShuffleMode(networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_InsideAlbum);
break;
case PlaylistSequence::ShuffleMode::Albums:
shuffle.setShuffleMode(networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_Albums);
break;
}
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::SHUFFLE);
msg.setShuffle(shuffle);
SendDataToClients(&msg);
}
void OutgoingDataCreator::UpdateTrackPosition() {
const qint64 position_nanosec = player_->engine()->position_nanosec();
int position = static_cast<int>(std::floor(static_cast<double>(position_nanosec) / kNsecPerSec + 0.5));
if (position_nanosec > current_song_.length_nanosec()) {
position = last_track_position_;
}
last_track_position_ = position;
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::UPDATE_TRACK_POSITION);
networkremote::ResponseUpdateTrackPosition reponse_update_track_position;
reponse_update_track_position.setPosition(position);
msg.setResponseUpdateTrackPosition(reponse_update_track_position);
SendDataToClients(&msg);
}
void OutgoingDataCreator::DisconnectAllClients() {
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::DISCONNECT);
networkremote::ResponseDisconnect reponse_disconnect;
reponse_disconnect.setReasonDisconnect(networkremote::ReasonDisconnectGadget::ReasonDisconnect::Server_Shutdown);
msg.setResponseDisconnect(reponse_disconnect);
SendDataToClients(&msg);
}
void OutgoingDataCreator::SendCollection(NetworkRemoteClient *client) {
const QString temp_database_filename = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + u'/' + Utilities::GetRandomStringWithChars(20);
Database::AttachedDatabase adb(temp_database_filename, ""_L1, true);
QSqlDatabase db(database_->Connect());
database_->AttachDatabaseOnDbConnection(u"songs_export"_s, adb, db);
SqlQuery q(db);
q.prepare(u"CREATE TABLE songs_export.songs AS SELECT * FROM songs WHERE unavailable = 0"_s);
if (!q.exec()) {
database_->ReportErrors(q);
return;
}
database_->DetachDatabase(u"songs_export"_s);
QFile file(temp_database_filename);
const QByteArray sha1 = Utilities::Sha1File(file).toHex();
qLog(Debug) << "Collection SHA1" << sha1;
if (!file.open(QIODevice::ReadOnly)) {
qLog(Error) << "Could not open file" << temp_database_filename;
}
const int chunk_count = qRound((file.size() / kFileChunkSize) + 0.5);
int chunk_number = 0;
while (!file.atEnd()) {
++chunk_number;
const QByteArray data = file.read(kFileChunkSize);
networkremote::ResponseCollectionChunk chunk;
chunk.setChunkNumber(chunk_number);
chunk.setChunkCount(chunk_count);
chunk.setSize(file.size());
chunk.setData(data);
chunk.setFileHash(sha1);
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::COLLECTION_CHUNK);
msg.setResponseCollectionChunk(chunk);
client->SendData(&msg);
}
file.remove();
file.close();
}
void OutgoingDataCreator::SendListFiles(QString relative_path, NetworkRemoteClient *client) {
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::LIST_FILES);
networkremote::ResponseListFiles files;
if (files_root_folder_.isEmpty()) {
files.setError(networkremote::ResponseListFiles::Error::ROOT_DIR_NOT_SET);
SendDataToClients(&msg);
return;
}
QDir root_dir(files_root_folder_);
if (!root_dir.exists()) {
files.setError(networkremote::ResponseListFiles::Error::ROOT_DIR_NOT_SET);
}
else if (relative_path.startsWith(".."_L1) || relative_path.startsWith("./.."_L1)) {
files.setError(networkremote::ResponseListFiles::Error::DIR_NOT_ACCESSIBLE);
}
else {
if (relative_path.startsWith("/"_L1)) relative_path.remove(0, 1);
QFileInfo fi_folder(root_dir, relative_path);
if (!fi_folder.exists()) {
files.setError(networkremote::ResponseListFiles::Error::DIR_NOT_EXIST);
}
else if (!fi_folder.isDir()) {
files.setError(networkremote::ResponseListFiles::Error::DIR_NOT_EXIST);
}
else if (root_dir.relativeFilePath(fi_folder.absoluteFilePath()).startsWith("../"_L1)) {
files.setError(networkremote::ResponseListFiles::Error::DIR_NOT_ACCESSIBLE);
}
else {
files.setRelativePath(root_dir.relativeFilePath(fi_folder.absoluteFilePath()));
QDir dir(fi_folder.absoluteFilePath());
dir.setFilter(QDir::NoDotAndDotDot | QDir::AllEntries);
dir.setSorting(QDir::Name | QDir::DirsFirst);
const QList<QFileInfo> fis = dir.entryInfoList();
for (const QFileInfo &fi : fis) {
if (fi.isDir() || files_music_extensions_.contains(fi.suffix())) {
networkremote::FileMetadata pb_file;// = files->addFiles();
pb_file.setIsDir(fi.isDir());
pb_file.setFilename(fi.fileName());
}
}
}
}
msg.setResponseListFiles(files);
client->SendData(&msg);
}

View File

@@ -1,120 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef OUTGOINGDATACREATOR_H
#define OUTGOINGDATACREATOR_H
#include <QObject>
#include <QList>
#include <QMap>
#include <QQueue>
#include <QString>
#include <QStringList>
#include <QImage>
#include <QTcpSocket>
#include <QTimer>
#include "includes/shared_ptr.h"
#include "engine/enginebase.h"
#include "playlist/playlistsequence.h"
#include "networkremotemessages.qpb.h"
#include "covermanager/albumcoverloaderresult.h"
class Database;
class Player;
class PlaylistManager;
class PlaylistBackend;
class Playlist;
class NetworkRemoteClient;
class OutgoingDataCreator : public QObject {
Q_OBJECT
public:
explicit OutgoingDataCreator(const SharedPtr<Database> database,
const SharedPtr<Player> player,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<PlaylistBackend> playlist_backend,
QObject *parent = nullptr);
~OutgoingDataCreator();
void SetClients(QList<NetworkRemoteClient*> *clients);
void SetRemoteRootFiles(const QString &files_root_folder) {
files_root_folder_ = files_root_folder;
}
void SetMusicExtensions(const QStringList &files_music_extensions) {
files_music_extensions_ = files_music_extensions;
}
void SetAllowDownloads(bool allow_downloads) {
allow_downloads_ = allow_downloads;
}
static networkremote::SongMetadata PbSongMetadataFromSong(const int index, const Song &song, const QImage &image_cover_art = QImage());
public Q_SLOTS:
void SendInfo();
void SendKeepAlive();
void SendAllPlaylists();
void SendAllActivePlaylists();
void SendFirstData(const bool send_playlist_songs);
void SendPlaylistSongs(const int id);
void PlaylistChanged(Playlist *playlist);
void VolumeChanged(const uint volume);
void PlaylistAdded(const int id, const QString &name, bool favorite);
void PlaylistDeleted(const int id);
void PlaylistClosed(const int id);
void PlaylistRenamed(const int id, const QString &new_name);
void ActiveChanged(Playlist *playlist);
void CurrentSongChanged(const Song &song, const AlbumCoverLoaderResult &result);
void SendSongMetadata();
void StateChanged(const EngineBase::State state);
void SendRepeatMode(const PlaylistSequence::RepeatMode mode);
void SendShuffleMode(const PlaylistSequence::ShuffleMode mode);
void UpdateTrackPosition();
void DisconnectAllClients();
void SendCollection(NetworkRemoteClient *client);
void SendListFiles(QString relative_path, NetworkRemoteClient *client);
private:
void SendDataToClients(networkremote::Message *msg);
networkremote::EngineStateGadget::EngineState GetEngineState();
private:
const SharedPtr<Database> database_;
const SharedPtr<Player> player_;
const SharedPtr<PlaylistManager> playlist_manager_;
const SharedPtr<PlaylistBackend> playlist_backend_;
QList<NetworkRemoteClient*> *clients_;
Song current_song_;
AlbumCoverLoaderResult albumcoverloader_result_;
QImage current_image_;
EngineBase::State last_state_;
QTimer *keep_alive_timer_;
QTimer *track_position_timer_;
int keep_alive_timeout_;
int last_track_position_;
QString files_root_folder_;
QStringList files_music_extensions_;
bool allow_downloads_;
};
#endif // OUTGOINGDATACREATOR_H

View File

@@ -1,442 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "songsender.h"
#include <QImage>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSettings>
#include "includes/shared_ptr.h"
#include "constants/networkremotesettingsconstants.h"
#include "constants/networkremoteconstants.h"
#include "core/logging.h"
#include "core/player.h"
#include "collection/collectionbackend.h"
#include "playlist/playlistmanager.h"
#include "playlist/playlist.h"
#include "networkremote.h"
#include "outgoingdatacreator.h"
#include "networkremoteclient.h"
#include "utilities/randutils.h"
#include "utilities/cryptutils.h"
using namespace Qt::Literals::StringLiterals;
using namespace NetworkRemoteSettingsConstants;
using namespace NetworkRemoteConstants;
SongSender::SongSender(const SharedPtr<Player> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> playlist_manager,
NetworkRemoteClient *client,
QObject *parent)
: QObject(parent),
player_(player),
collection_backend_(collection_backend),
playlist_manager_(playlist_manager),
client_(client),
transcoder_(new Transcoder(this, QLatin1String(kTranscoderSettingPostfix))) {
QSettings s;
s.beginGroup(kSettingsGroup);
transcode_lossless_files_ = s.value("convert_lossless", false).toBool();
// Load preset
QString last_output_format = s.value("last_output_format", u"audio/x-vorbis"_s).toString();
QList<TranscoderPreset> presets = transcoder_->GetAllPresets();
for (int i = 0; i < presets.count(); ++i) {
if (last_output_format == presets.at(i).codec_mimetype_) {
transcoder_preset_ = presets.at(i);
break;
}
}
qLog(Debug) << "Transcoder preset" << transcoder_preset_.codec_mimetype_;
QObject::connect(transcoder_, &Transcoder::JobComplete, this, &SongSender::TranscodeJobComplete);
QObject::connect(transcoder_, &Transcoder::AllJobsComplete, this, &SongSender::StartTransfer);
total_transcode_ = 0;
}
SongSender::~SongSender() {
QObject::disconnect(transcoder_, &Transcoder::JobComplete, this, &SongSender::TranscodeJobComplete);
QObject::disconnect(transcoder_, &Transcoder::AllJobsComplete, this, &SongSender::StartTransfer);
transcoder_->Cancel();
}
void SongSender::SendSongs(const networkremote::RequestDownloadSongs &request) {
Song current_song;
if (player_->GetCurrentItem()) {
current_song = player_->GetCurrentItem()->Metadata();
}
switch (request.downloadItem()) {
case networkremote::DownloadItemGadget::DownloadItem::CurrentItem:{
if (current_song.is_valid()) {
const DownloadItem item(current_song, 1, 1);
download_queue_.append(item);
}
break;
}
case networkremote::DownloadItemGadget::DownloadItem::ItemAlbum:
if (current_song.is_valid()) {
SendAlbum(current_song);
}
break;
case networkremote::DownloadItemGadget::DownloadItem::APlaylist:
SendPlaylist(request);
break;
case networkremote::DownloadItemGadget::DownloadItem::Urls:
SendUrls(request);
break;
default:
break;
}
if (transcode_lossless_files_) {
TranscodeLosslessFiles();
}
else {
StartTransfer();
}
}
void SongSender::TranscodeLosslessFiles() {
for (const DownloadItem &item : std::as_const(download_queue_)) {
// Check only lossless files
if (!item.song_.IsFileLossless()) continue;
// Add the file to the transcoder
const QString local_file = item.song_.url().toLocalFile();
qLog(Debug) << "Transcoding" << local_file;
transcoder_->AddJob(local_file, transcoder_preset_, Utilities::GetRandomStringWithCharsAndNumbers(20));
total_transcode_++;
}
if (total_transcode_ > 0) {
transcoder_->Start();
SendTranscoderStatus();
}
else {
StartTransfer();
}
}
void SongSender::TranscodeJobComplete(const QString &input, const QString &output, const bool success) {
qLog(Debug) << input << "transcoded to" << output << success;
// If it wasn't successful send original file
if (success) {
transcoder_map_.insert(input, output);
}
SendTranscoderStatus();
}
void SongSender::SendTranscoderStatus() {
// Send a message to the remote that we are converting files
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::TRANSCODING_FILES);
networkremote::ResponseTranscoderStatus status = msg.responseTranscoderStatus();
status.setProcessed(static_cast<int>(transcoder_map_.count()));
status.setTotal(total_transcode_);
client_->SendData(&msg);
}
void SongSender::StartTransfer() {
total_transcode_ = 0;
// Send total file size & file count
SendTotalFileSize();
// Send first file
OfferNextSong();
}
void SongSender::SendTotalFileSize() {
networkremote::Message msg;
msg.setType(networkremote::MsgTypeGadget::MsgType::DOWNLOAD_TOTAL_SIZE);
networkremote::ResponseDownloadTotalSize response = msg.responseDownloadTotalSize();
response.setFileCount(download_queue_.size());
qint64 total = 0;
for (const DownloadItem &item : std::as_const(download_queue_)) {
QString local_file = item.song_.url().toLocalFile();
const bool is_transcoded = transcoder_map_.contains(local_file);
if (is_transcoded) {
local_file = transcoder_map_.value(local_file);
}
total += QFileInfo(local_file).size();
}
response.setTotalSize(total);
client_->SendData(&msg);
}
void SongSender::OfferNextSong() {
networkremote::Message msg;
if (download_queue_.isEmpty()) {
msg.setType(networkremote::MsgTypeGadget::MsgType::DOWNLOAD_QUEUE_EMPTY);
}
else {
// Get the item and send the single song
const DownloadItem item = download_queue_.head();
msg.setType(networkremote::MsgTypeGadget::MsgType::SONG_OFFER_FILE_CHUNK);
networkremote::ResponseSongFileChunk chunk = msg.responseSongFileChunk();
// Open the file
QFile file(item.song_.url().toLocalFile());
// Song offer is chunk no 0
chunk.setChunkCount(0);
chunk.setChunkNumber(0);
chunk.setFileCount(item.song_count_);
chunk.setFileNumber(item.song_number_);
chunk.setSize(file.size());
chunk.setSongMetadata(OutgoingDataCreator::PbSongMetadataFromSong(-1, item.song_));
msg.setResponseSongFileChunk(chunk);
}
client_->SendData(&msg);
}
void SongSender::ResponseSongOffer(const bool accepted) {
if (download_queue_.isEmpty()) return;
// Get the item and send the single song
DownloadItem item = download_queue_.dequeue();
if (accepted) SendSingleSong(item);
// And offer the next song
OfferNextSong();
}
void SongSender::SendSingleSong(const DownloadItem &download_item) {
if (!download_item.song_.url().isLocalFile()) return;
QString local_file = download_item.song_.url().toLocalFile();
bool is_transcoded = transcoder_map_.contains(local_file);
if (is_transcoded) {
local_file = transcoder_map_.take(local_file);
}
// Open the file
QFile file(local_file);
// Get sha1 for file
QByteArray sha1 = Utilities::Sha1File(file).toHex();
qLog(Debug) << "sha1 for file" << local_file << "=" << sha1;
file.open(QIODevice::ReadOnly);
QByteArray data;
networkremote::Message msg;
networkremote::ResponseSongFileChunk chunk = msg.responseSongFileChunk();
msg.setType(networkremote::MsgTypeGadget::MsgType::SONG_OFFER_FILE_CHUNK);
// Calculate the number of chunks
int chunk_count = qRound((static_cast<quint32>(file.size()) / kFileChunkSize) + 0.5);
int chunk_number = 1;
while (!file.atEnd()) {
// Read file chunk
data = file.read(kFileChunkSize);
// Set chunk data
chunk.setChunkCount(chunk_count);
chunk.setChunkNumber(chunk_number);
chunk.setFileCount(download_item.song_count_);
chunk.setFileNumber(download_item.song_number_);
chunk.setSize(file.size());
chunk.setData(data);
chunk.setFileHash(sha1);
// On the first chunk send the metadata, so the client knows what file it receives.
if (chunk_number == 1) {
const int i = playlist_manager_->active()->current_row();
networkremote::SongMetadata song_metadata = OutgoingDataCreator::PbSongMetadataFromSong(i, download_item.song_);
// If the file was transcoded, we have to change the filename and filesize
if (is_transcoded) {
song_metadata.setFileSize(file.size());
QString basefilename = download_item.song_.basefilename();
QFileInfo info(basefilename);
basefilename.replace(u'.' + info.suffix(), u'.' + transcoder_preset_.extension_);
song_metadata.setFilename(basefilename);
}
}
// Send data directly to the client
client_->SendData(&msg);
// Clear working data
chunk = networkremote::ResponseSongFileChunk();
data.clear();
chunk_number++;
}
// If the file was transcoded, delete the temporary one
if (is_transcoded) {
file.remove();
}
else {
file.close();
}
}
void SongSender::SendAlbum(const Song &album_song) {
if (!album_song.url().isLocalFile()) return;
const SongList songs = collection_backend_->GetSongsByAlbum(album_song.album());
for (const Song &song : songs) {
const DownloadItem item(song, static_cast<int>(songs.indexOf(song)) + 1, static_cast<int>(songs.size()));
download_queue_.append(item);
}
}
void SongSender::SendPlaylist(const networkremote::RequestDownloadSongs &request) {
const int playlist_id = request.playlistId();
Playlist *playlist = playlist_manager_->playlist(playlist_id);
if (!playlist) {
qLog(Info) << "Could not find playlist with id = " << playlist_id;
return;
}
const SongList song_list = playlist->GetAllSongs();
QList<int> requested_ids;
requested_ids.reserve(request.songsIds().count());
for (auto song_id : request.songsIds()) {
requested_ids << song_id;
}
// Count the local songs
int count = 0;
for (const Song &song : song_list) {
if (song.url().isLocalFile() && (requested_ids.isEmpty() || requested_ids.contains(song.id()))) {
++count;
}
}
for (const Song &song : song_list) {
if (song.url().isLocalFile() && (requested_ids.isEmpty() || requested_ids.contains(song.id()))) {
DownloadItem item(song, static_cast<int>(song_list.indexOf(song)) + 1, count);
download_queue_.append(item);
}
}
}
void SongSender::SendUrls(const networkremote::RequestDownloadSongs &request) {
SongList songs;
// First gather all valid songs
if (!request.relativePath().isEmpty()) {
// Security checks, cf OutgoingDataCreator::SendListFiles
const QString &files_root_folder = client_->files_root_folder();
if (files_root_folder.isEmpty()) return;
QDir root_dir(files_root_folder);
QString relative_path = request.relativePath();
if (!root_dir.exists() || relative_path.startsWith(".."_L1) || relative_path.startsWith("./.."_L1))
return;
if (relative_path.startsWith(u'/')) relative_path.remove(0, 1);
QFileInfo fi_folder(root_dir, relative_path);
if (!fi_folder.exists() || !fi_folder.isDir() || root_dir.relativeFilePath(fi_folder.absoluteFilePath()).startsWith(u"../"_s)) {
return;
}
QDir dir(fi_folder.absoluteFilePath());
const QStringList &files_music_extensions = client_->files_music_extensions();
for (const QString &s : request.urls()) {
QFileInfo fi(dir, s);
if (fi.exists() && fi.isFile() && files_music_extensions.contains(fi.suffix())) {
Song song;
song.set_basefilename(fi.fileName());
song.set_filesize(fi.size());
song.set_url(QUrl::fromLocalFile(fi.absoluteFilePath()));
song.set_valid(true);
songs.append(song);
}
}
}
else {
for (const QString &url_str : request.urls()) {
const QUrl url(url_str);
Song song = collection_backend_->GetSongByUrl(url);
if (song.is_valid() && song.url().isLocalFile()) {
songs.append(song);
}
}
}
for (const Song &song : songs) {
DownloadItem item(song, static_cast<int>(songs.indexOf(song)) + 1, static_cast<int>(songs.count()));
download_queue_.append(item);
}
}

View File

@@ -1,96 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SONGSENDER_H
#define SONGSENDER_H
#include <QObject>
#include <QMap>
#include <QQueue>
#include <QString>
#include <QUrl>
#include "includes/shared_ptr.h"
#include "core/song.h"
#include "networkremotemessages.qpb.h"
#include "transcoder/transcoder.h"
class Player;
class CollectionBackend;
class PlaylistManager;
class NetworkRemoteClient;
class Transcoder;
class DownloadItem {
public:
explicit DownloadItem(const Song &song, const int song_number, const int song_count)
: song_(song), song_number_(song_number), song_count_(song_count) {}
Song song_;
int song_number_;
int song_count_;
};
class SongSender : public QObject {
Q_OBJECT
public:
explicit SongSender(const SharedPtr<Player> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> playlist_manager,
NetworkRemoteClient *client,
QObject *parent = nullptr);
~SongSender();
public Q_SLOTS:
void SendSongs(const networkremote::RequestDownloadSongs &request);
void ResponseSongOffer(bool accepted);
private Q_SLOTS:
void TranscodeJobComplete(const QString &input, const QString &output, const bool success);
void StartTransfer();
private:
const SharedPtr<Player> player_;
const SharedPtr<CollectionBackend> collection_backend_;
const SharedPtr<PlaylistManager> playlist_manager_;
NetworkRemoteClient *client_;
TranscoderPreset transcoder_preset_;
Transcoder *transcoder_;
bool transcode_lossless_files_;
QQueue<DownloadItem> download_queue_;
QMap<QString, QString> transcoder_map_;
int total_transcode_;
void SendSingleSong(const DownloadItem &download_item);
void SendAlbum(const Song &song);
void SendPlaylist(const networkremote::RequestDownloadSongs &request);
void SendUrls(const networkremote::RequestDownloadSongs &request);
void OfferNextSong();
void SendTotalFileSize();
void TranscodeLosslessFiles();
void SendTranscoderStatus();
};
#endif // SONGSENDER_H

View File

@@ -57,7 +57,6 @@
#include "playlistview.h"
#include "playlistsaveoptionsdialog.h"
#include "playlistparsers/playlistparser.h"
#include "queue/queue.h"
#include "dialogs/saveplaylistsdialog.h"
using namespace Qt::Literals::StringLiterals;
@@ -186,9 +185,9 @@ Playlist *PlaylistManager::AddPlaylist(const int id, const QString &name, const
}
int PlaylistManager::New(const QString &name, const SongList &songs, const QString &special_type) {
void PlaylistManager::New(const QString &name, const SongList &songs, const QString &special_type) {
if (name.isNull()) return -1;
if (name.isNull()) return;
int id = playlist_backend_->CreatePlaylist(name, special_type);
@@ -204,8 +203,6 @@ int PlaylistManager::New(const QString &name, const SongList &songs, const QStri
Rename(id, QStringLiteral("%1 %2").arg(name).arg(id));
}
return id;
}
void PlaylistManager::Load(const QString &filename) {
@@ -617,22 +614,3 @@ void PlaylistManager::SaveAllPlaylists() {
}
}
void PlaylistManager::Clear(const int id) {
if (playlists_.count() <= 1 || !playlists_.contains(id)) return;
playlists_[id].p->Clear();
}
void PlaylistManager::Enqueue(const int id, const int i) {
QModelIndexList dummyIndexList;
Q_ASSERT(playlists_.contains(id));
dummyIndexList.append(playlist(id)->index(i, 0));
playlist(id)->queue()->ToggleTracks(dummyIndexList);
}

View File

@@ -95,7 +95,7 @@ class PlaylistManager : public PlaylistManagerInterface {
PlaylistContainer *playlist_container() const override { return playlist_container_; }
public Q_SLOTS:
int New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()) override;
void New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()) override;
void Load(const QString &filename) override;
void Save(const int id, const QString &playlist_name, const QString &filename, const PlaylistSettings::PathType path_type) override;
// Display a file dialog to let user choose a file before saving the file
@@ -144,9 +144,6 @@ class PlaylistManager : public PlaylistManagerInterface {
void SetActivePaused() override;
void SetActiveStopped() override;
void Clear(const int id);
void Enqueue(const int id, const int i);
private Q_SLOTS:
void OneOfPlaylistsChanged();
void UpdateSummaryText();

View File

@@ -77,7 +77,7 @@ class PlaylistManagerInterface : public QObject {
virtual void PlaySmartPlaylist(PlaylistGeneratorPtr generator, const bool as_new, const bool clear) = 0;
public Q_SLOTS:
virtual int New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()) = 0;
virtual void New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()) = 0;
virtual void Load(const QString &filename) = 0;
virtual void Save(const int id, const QString &playlist_name, const QString &filename, const PlaylistSettings::PathType path_type) = 0;
virtual void Rename(const int id, const QString &new_name) = 0;

View File

@@ -51,6 +51,7 @@ class Queue : public QAbstractProxyModel {
// Modify the queue
int TakeNext();
void ToggleTracks(const QModelIndexList &source_indexes);
void InsertFirst(const QModelIndexList &source_indexes);
void Clear();
void Move(const QList<int> &proxy_rows, int pos);
@@ -78,7 +79,6 @@ class Queue : public QAbstractProxyModel {
public Q_SLOTS:
void UpdateSummaryText();
void ToggleTracks(const QModelIndexList &source_indexes);
Q_SIGNALS:
void TotalLengthChanged(const quint64 length);

View File

@@ -1,157 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "networkremotesettingspage.h"
#include <algorithm>
#include <QString>
#include <QUrl>
#include <QFile>
#include <QHostInfo>
#include <QNetworkInterface>
#include <QDesktopServices>
#include <QSettings>
#include <QRandomGenerator>
#include "constants/networkremotesettingsconstants.h"
#include "constants/networkremoteconstants.h"
#include "core/iconloader.h"
#include "networkremote/networkremote.h"
#include "transcoder/transcoder.h"
#include "transcoder/transcoderoptionsdialog.h"
#include "settingsdialog.h"
#include "ui_networkremotesettingspage.h"
using namespace Qt::Literals::StringLiterals;
using namespace NetworkRemoteSettingsConstants;
using namespace NetworkRemoteConstants;
namespace {
static bool ComparePresetsByName(const TranscoderPreset &left, const TranscoderPreset &right) {
return left.name_ < right.name_;
}
} // namespace
NetworkRemoteSettingsPage::NetworkRemoteSettingsPage(SettingsDialog *dialog)
: SettingsPage(dialog),
ui_(new Ui_NetworkRemoteSettingsPage) {
ui_->setupUi(this);
setWindowIcon(IconLoader::Load(u"ipodtouchicon"_s));
QObject::connect(ui_->options, &QPushButton::clicked, this, &NetworkRemoteSettingsPage::Options);
QList<TranscoderPreset> presets = Transcoder::GetAllPresets();
std::sort(presets.begin(), presets.end(), ComparePresetsByName);
for (const TranscoderPreset &preset : std::as_const(presets)) {
ui_->format->addItem(QStringLiteral("%1 (.%2)").arg(preset.name_, preset.extension_), QVariant::fromValue(preset));
}
}
NetworkRemoteSettingsPage::~NetworkRemoteSettingsPage() { delete ui_; }
void NetworkRemoteSettingsPage::Load() {
QSettings s;
s.beginGroup(kSettingsGroup);
ui_->enabled->setChecked(s.value(kEnabled).toBool());
ui_->spinbox_port->setValue(s.value(kPort, kDefaultServerPort).toInt());
ui_->checkbox_allow_public_access->setChecked(s.value(kAllowPublicAccess, false).toBool());
ui_->checkbox_use_auth_code->setChecked(s.value(kUseAuthCode, false).toBool());
ui_->spinbox_auth_code->setValue(s.value(kAuthCode, QRandomGenerator::global()->bounded(100000)).toInt());
ui_->allow_downloads->setChecked(s.value("allow_downloads", false).toBool());
ui_->convert_lossless->setChecked(s.value("convert_lossless", false).toBool());
QString last_output_format = s.value("last_output_format", u"audio/x-vorbis"_s).toString();
for (int i = 0; i < ui_->format->count(); ++i) {
if (last_output_format == ui_->format->itemData(i).value<TranscoderPreset>().codec_mimetype_) {
ui_->format->setCurrentIndex(i);
break;
}
}
ui_->files_root_folder->SetPath(s.value("files_root_folder").toString());
ui_->files_music_extensions->setText(s.value("files_music_extensions", kDefaultMusicExtensionsAllowedRemotely).toStringList().join(u','));
s.endGroup();
// Get local IP addresses
QString ip_addresses;
QList<QHostAddress> addresses = QNetworkInterface::allAddresses();
for (const QHostAddress &address : addresses) {
// TODO: Add IPv6 support to tinysvcmdns
if (address.protocol() == QAbstractSocket::IPv4Protocol && !address.isInSubnet(QHostAddress::parseSubnet(u"127.0.0.1/8"_s))) {
if (!ip_addresses.isEmpty()) {
ip_addresses.append(u", "_s);
}
ip_addresses.append(address.toString());
}
}
ui_->label_ip_address->setText(ip_addresses);
}
void NetworkRemoteSettingsPage::Save() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue(kEnabled, ui_->enabled->isChecked());
s.setValue(kPort, ui_->spinbox_port->value());
s.setValue(kAllowPublicAccess, ui_->checkbox_allow_public_access->isChecked());
s.setValue(kUseAuthCode, ui_->checkbox_use_auth_code->isChecked());
s.setValue(kAuthCode, ui_->spinbox_auth_code->value());
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex()).value<TranscoderPreset>();
s.setValue("last_output_format", preset.codec_mimetype_);
s.setValue(kFilesRootFolder, ui_->files_root_folder->Path());
QStringList files_music_extensions;
for (const QString &extension : ui_->files_music_extensions->text().split(u',')) {
QString ext = extension.trimmed();
if (ext.size() > 0 && ext.size() < 8) // no empty string, less than 8 char
files_music_extensions << ext;
}
s.setValue("files_music_extensions", files_music_extensions);
s.endGroup();
}
void NetworkRemoteSettingsPage::Options() {
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex()).value<TranscoderPreset>();
TranscoderOptionsDialog dialog(preset.filetype_, this);
dialog.set_settings_postfix(QLatin1String(kTranscoderSettingPostfix));
dialog.exec();
}

View File

@@ -1,46 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef NETWORKREMOTESETTINGSPAGE_H
#define NETWORKREMOTESETTINGSPAGE_H
#include "settingspage.h"
class Ui_NetworkRemoteSettingsPage;
class NetworkRemoteSettingsPage : public SettingsPage {
Q_OBJECT
public:
explicit NetworkRemoteSettingsPage(SettingsDialog *dialog);
~NetworkRemoteSettingsPage();
void Load();
void Save();
private Q_SLOTS:
void Options();
private:
Ui_NetworkRemoteSettingsPage *ui_;
};
#endif // NETWORKREMOTESETTINGSPAGE_H

View File

@@ -1,298 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NetworkRemoteSettingsPage</class>
<widget class="QWidget" name="NetworkRemoteSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>421</width>
<height>664</height>
</rect>
</property>
<property name="windowTitle">
<string>Network Remote</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="enabled">
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupbox_use_remote_container">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Settings</string>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_port">
<property name="minimumSize">
<size>
<width>171</width>
<height>0</height>
</size>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Port</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinbox_port">
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>8080</number>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="checkbox_allow_public_access">
<property name="toolTip">
<string>Only accept connections from clients within the ip ranges:
10.x.x.x
172.16.0.0 - 172.31.255.255
192.168.x.x</string>
</property>
<property name="text">
<string>Allow public access</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="checkbox_use_auth_code">
<property name="toolTip">
<string>A client can connect only, if the correct code was entered.</string>
</property>
<property name="text">
<string>Require authentication code</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinbox_auth_code">
<property name="enabled">
<bool>false</bool>
</property>
<property name="suffix">
<string/>
</property>
<property name="maximum">
<number>99999</number>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_ip_address_description">
<property name="toolTip">
<string>Enter this IP in the App to connect to Clementine.</string>
</property>
<property name="text">
<string>Your IP address:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QLabel" name="label_ip_address">
<property name="text">
<string notr="true">127.0.0.1</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="allow_downloads">
<property name="toolTip">
<string>Allow a client to download music from this computer.</string>
</property>
<property name="text">
<string>Allow downloads</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QGroupBox" name="download_settings_container">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Download settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="convert_lossless">
<property name="toolTip">
<string>Convert lossless audiofiles before sending them to the remote.</string>
</property>
<property name="text">
<string>Convert lossless files</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="format_container">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Audio format</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QPushButton" name="options">
<property name="text">
<string>Options...</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="format"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_files_root_folder">
<property name="toolTip">
<string>Root folder that will be browsable from the network remote</string>
</property>
<property name="text">
<string>Files root folder</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="FileChooserWidget" name="files_root_folder" native="true">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="files_music_extensions"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_music_extensions">
<property name="toolTip">
<string>comma-separated list of the allowed extensions that will be visible from the network remote (ex: m3u,mp3,flac,ogg,wav)</string>
</property>
<property name="text">
<string>Music extensions remotely visible</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="spacer_bottom">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>98</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>FileChooserWidget</class>
<extends>QWidget</extends>
<header location="global">widgets/filechooserwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>checkbox_use_auth_code</sender>
<signal>toggled(bool)</signal>
<receiver>spinbox_auth_code</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>137</x>
<y>124</y>
</hint>
<hint type="destinationlabel">
<x>351</x>
<y>125</y>
</hint>
</hints>
</connection>
<connection>
<sender>enabled</sender>
<signal>toggled(bool)</signal>
<receiver>groupbox_use_remote_container</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>59</x>
<y>22</y>
</hint>
<hint type="destinationlabel">
<x>57</x>
<y>43</y>
</hint>
</hints>
</connection>
<connection>
<sender>allow_downloads</sender>
<signal>toggled(bool)</signal>
<receiver>download_settings_container</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>196</x>
<y>160</y>
</hint>
<hint type="destinationlabel">
<x>117</x>
<y>205</y>
</hint>
</hints>
</connection>
<connection>
<sender>convert_lossless</sender>
<signal>toggled(bool)</signal>
<receiver>format_container</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>218</x>
<y>212</y>
</hint>
<hint type="destinationlabel">
<x>218</x>
<y>262</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -90,9 +90,6 @@
# include "qobuz/qobuzservice.h"
# include "qobuzsettingspage.h"
#endif
#ifdef HAVE_NETWORKREMOTE
# include "networkremotesettingspage.h"
#endif
#include "ui_settingsdialog.h"
@@ -164,10 +161,6 @@ SettingsDialog::SettingsDialog(const SharedPtr<Player> player,
AddPage(Page::Qobuz, new QobuzSettingsPage(this, streaming_services->Service<QobuzService>(), this), streaming);
#endif
#ifdef HAVE_NETWORKREMOTE
AddPage(Page::NetworkRemote, new NetworkRemoteSettingsPage(this));
#endif
// List box
QObject::connect(ui_->list, &QTreeWidget::currentItemChanged, this, &SettingsDialog::CurrentItemChanged);
ui_->list->setCurrentItem(pages_[Page::Behaviour].item_);

View File

@@ -93,7 +93,6 @@ class SettingsDialog : public QDialog {
Tidal,
Qobuz,
Spotify,
NetworkRemote
};
enum Role {

View File

@@ -20,8 +20,6 @@
#include <QByteArray>
#include <QString>
#include <QCryptographicHash>
#include <QFile>
#include <QIODevice>
#include "cryptutils.h"
@@ -64,17 +62,4 @@ QByteArray HmacSha1(const QByteArray &key, const QByteArray &data) {
return Hmac(key, data, QCryptographicHash::Sha1);
}
QByteArray Sha1File(QFile &file) {
file.open(QIODevice::ReadOnly);
QCryptographicHash hash(QCryptographicHash::Sha1);
while (!file.atEnd()) {
hash.addData(file.read(1000000));
}
file.close();
return hash.result();
}
} // namespace Utilities

View File

@@ -23,7 +23,6 @@
#include <QByteArray>
#include <QString>
#include <QCryptographicHash>
#include <QFile>
namespace Utilities {
@@ -31,7 +30,6 @@ QByteArray Hmac(const QByteArray &key, const QByteArray &data, const QCryptograp
QByteArray HmacMd5(const QByteArray &key, const QByteArray &data);
QByteArray HmacSha256(const QByteArray &key, const QByteArray &data);
QByteArray HmacSha1(const QByteArray &key, const QByteArray &data);
QByteArray Sha1File(QFile &file);
} // namespace Utilities

View File

@@ -1,116 +0,0 @@
#include <QFileDialog>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include "filechooserwidget.h"
using namespace Qt::Literals::StringLiterals;
FileChooserWidget::FileChooserWidget(QWidget *parent)
: QWidget(parent),
layout_(new QHBoxLayout(this)),
path_edit_(new QLineEdit(this)),
mode_(Mode::Directory) {
Init();
}
FileChooserWidget::FileChooserWidget(const Mode mode, const QString &initial_path, QWidget* parent)
: QWidget(parent),
layout_(new QHBoxLayout(this)),
path_edit_(new QLineEdit(this)),
mode_(mode) {
Init(initial_path);
}
FileChooserWidget::FileChooserWidget(const Mode mode, const QString &label, const QString &initial_path, QWidget* parent)
: QWidget(parent),
layout_(new QHBoxLayout(this)),
path_edit_(new QLineEdit(this)),
mode_(mode) {
layout_->addWidget(new QLabel(label, this));
Init(initial_path);
}
void FileChooserWidget::SetFileFilter(const QString &file_filter) {
file_filter_ = file_filter;
}
void FileChooserWidget::SetPath(const QString &path) {
QFileInfo fi(path);
if (fi.exists()) {
path_edit_->setText(path);
open_dir_path_ = fi.absolutePath();
}
}
QString FileChooserWidget::Path() const {
QString path(path_edit_->text());
QFileInfo fi(path);
if (!fi.exists()) return QString();
if (mode_ == Mode::File) {
if (!fi.isFile()) return QString();
}
else {
if (!fi.isDir()) return QString();
}
return path;
}
void FileChooserWidget::Init(const QString &initial_path) {
QFileInfo fi(initial_path);
if (fi.exists()) {
path_edit_->setText(initial_path);
open_dir_path_ = fi.absolutePath();
}
layout_->addWidget(path_edit_);
QPushButton* changePath = new QPushButton(QLatin1String("..."), this);
connect(changePath, &QAbstractButton::clicked, this, &FileChooserWidget::ChooseFile);
changePath->setFixedWidth(2 * changePath->fontMetrics().horizontalAdvance(" ... "_L1));
layout_->addWidget(changePath);
layout_->setContentsMargins(2, 0, 2, 0);
setFocusProxy(path_edit_);
}
void FileChooserWidget::ChooseFile() {
QString new_path;
if (mode_ == Mode::File) {
new_path = QFileDialog::getOpenFileName(this, tr("Select a file"), open_dir_path_, file_filter_);
}
else {
new_path = QFileDialog::getExistingDirectory(this, tr("Select a directory"), open_dir_path_);
}
if (!new_path.isEmpty()) {
QFileInfo fi(new_path);
open_dir_path_ = fi.absolutePath();
if (mode_ == Mode::File) {
path_edit_->setText(fi.absoluteFilePath());
}
else {
path_edit_->setText(fi.absoluteFilePath() + u"/"_s);
}
}
}

View File

@@ -1,40 +0,0 @@
#ifndef FILECHOOSERWIDGET_H
#define FILECHOOSERWIDGET_H
#include <QWidget>
class QLineEdit;
class QHBoxLayout;
class FileChooserWidget : public QWidget {
Q_OBJECT
public:
enum class Mode { File, Directory };
public:
explicit FileChooserWidget(QWidget *parent = nullptr);
explicit FileChooserWidget(const Mode mode, const QString& initial_path = QString(), QWidget *parent = nullptr);
explicit FileChooserWidget(const Mode mode, const QString& label, const QString &initial_path = QString(), QWidget *parent = nullptr);
~FileChooserWidget() = default;
void SetFileFilter(const QString &file_filter);
void SetPath(const QString &path);
QString Path() const;
public Q_SLOTS:
void ChooseFile();
private:
void Init(const QString &initial_path = QString());
private:
QHBoxLayout *layout_;
QLineEdit *path_edit_;
const Mode mode_;
QString file_filter_;
QString open_dir_path_;
};
#endif // FILECHOOSERWIDGET_H