Compare commits

..

1 Commits

Author SHA1 Message Date
Jonas Kvinge
38d49ceb64 Split into separate libraries 2025-01-22 18:34:04 +01:00
471 changed files with 10183 additions and 33592 deletions

View File

@@ -1 +0,0 @@
blank_issues_enabled: false

View File

@@ -68,7 +68,6 @@ jobs:
hicolor-icon-theme
qt6-core-devel
qt6-gui-devel
qt6-gui-private-devel
qt6-widgets-devel
qt6-concurrent-devel
qt6-network-devel
@@ -80,22 +79,9 @@ jobs:
qt6-linguist-devel
gtest
gmock
sparsehash-devel
rapidjson-devel
- name: Install kdsingleapplication-qt6-devel
if: matrix.opensuse_version == 'tumbleweed'
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
- name: Build and install KDSingleApplication
if: matrix.opensuse_version == 'leap:15.6'
env:
CC: gcc-14
CXX: g++-14
run: |
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
cd KDSingleApplication
cmake -S . -B build -DCMAKE_BUILD_TYPE="Release" -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON -DKDSingleApplication_STATIC=ON
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v4
with:
@@ -206,8 +192,6 @@ jobs:
kdsingleapplication-qt6-devel
gtest-devel
gmock-devel
sparsehash-devel
rapidjson-devel
- name: Checkout
uses: actions/checkout@v4
with:
@@ -244,7 +228,7 @@ jobs:
build-openmandriva:
name: Build OpenMandriva
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master' && false
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -259,30 +243,31 @@ jobs:
run: >
dnf install -y
which
glibc
gcc-c++
git
gnutar
make
cmake
glib
lsb-release
rpmdevtools
rpm-build
glibc-devel
boost-devel
lib64glib2.0-devel
lib64asound-devel
lib64pulseaudio-devel
lib64sqlite3-devel
lib64gstreamer-devel
lib64gst-plugins-base1.0-devel
lib64taglib-devel
lib64chromaprint-devel
lib64ebur128-devel
lib64fftw-devel
lib64icu-devel
lib64cdio-devel
lib64gpod-devel
lib64mtp-devel
sqlite-devel
libasound-devel
pulseaudio-devel
libGL-devel
libgst-plugins-base1.0-devel
taglib-devel
chromaprint-devel
libebur128-devel
fftw-devel
icu-devel
libcdio-devel
libgpod-devel
libmtp-devel
lib64Qt6Core-devel
lib64Qt6Concurrent-devel
lib64Qt6Network-devel
@@ -291,11 +276,6 @@ jobs:
lib64Qt6Gui-devel
lib64Qt6Widgets-devel
lib64Qt6Test-devel
lib64kdsingleapplication-devel
lib64xkbcommon-devel
lib64gtest-devel
lib64gmock-devel
sparsehash-devel
qt6-cmake
qt6-qtbase-tools
qt6-qttools-linguist
@@ -303,7 +283,6 @@ jobs:
appstream
appstream-util
hicolor-icon-theme
rapidjson
- name: Remove files
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
- name: Checkout
@@ -352,6 +331,12 @@ jobs:
container:
image: mageia:${{matrix.mageia_version}}
steps:
- name: Set media
run: |
urpmi.removemedia "Core Release"
urpmi.removemedia "Core Updates"
urpmi.addmedia "Core Release" "https://mirrors.kernel.org/mageia/distrib/${{matrix.mageia_version}}/x86_64/media/core/release/"
urpmi.addmedia "Core Updates" "https://mirrors.kernel.org/mageia/distrib/${{matrix.mageia_version}}/x86_64/media/core/updates/"
- name: Update repositories
run: urpmi.update -a
- name: Upgrade packages
@@ -393,21 +378,10 @@ jobs:
lib64qt6dbus-devel
lib64qt6help-devel
lib64qt6test-devel
lib64sparsehash-devel
lib64kdsingleapplication-devel
desktop-file-utils
appstream-util
hicolor-icon-theme
gtest
rapidjson
- name: Build and install KDSingleApplication
if: matrix.mageia_version == '9'
run: |
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
cd KDSingleApplication
cmake -S . -B build -DCMAKE_BUILD_TYPE="Release" -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON -DKDSingleApplication_STATIC=ON
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v4
with:
@@ -490,22 +464,11 @@ jobs:
libcdio-dev
libmtp-dev
libgpod-dev
libxkbcommon-dev
libsparsehash-dev
qt6-base-dev
qt6-base-private-dev
qt6-base-dev-tools
qt6-tools-dev
qt6-tools-dev-tools
qt6-l10n-tools
rapidjson-dev
- name: Build and install KDSingleApplication
run: |
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
cd KDSingleApplication
cmake -S . -B build -DCMAKE_BUILD_TYPE="Release" -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON -DKDSingleApplication_STATIC=ON
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v4
with:
@@ -582,22 +545,11 @@ jobs:
libcdio-dev
libmtp-dev
libgpod-dev
libxkbcommon-dev
libsparsehash-dev
qt6-base-dev
qt6-base-private-dev
qt6-base-dev-tools
qt6-tools-dev
qt6-tools-dev-tools
qt6-l10n-tools
rapidjson-dev
- name: Build and install KDSingleApplication
run: |
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
cd KDSingleApplication
cmake -S . -B build -DCMAKE_BUILD_TYPE="Release" -DBUILD_SHARED_LIBS=OFF -DBUILD_STATIC_LIBS=ON -DKDSingleApplication_STATIC=ON
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v4
with:
@@ -673,18 +625,13 @@ jobs:
libcdio-dev
libmtp-dev
libgpod-dev
libxkbcommon-dev
libsparsehash-dev
qt6-base-dev
qt6-base-private-dev
qt6-base-dev-tools
qt6-tools-dev
qt6-tools-dev-tools
qt6-l10n-tools
gstreamer1.0-alsa
gstreamer1.0-pulseaudio
libkdsingleapplication-qt6-dev
rapidjson-dev
- name: Install keyboxd
if: matrix.ubuntu_version == 'noble'
env:
@@ -697,10 +644,6 @@ jobs:
submodules: recursive
- name: Add safe git directory
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
- name: Modify required KDSingleApplication version
run: |
sed -i 's/^find_package(KDSingleApplication-qt\${QT_VERSION_MAJOR} 1.1.0 REQUIRED)$/find_package(KDSingleApplication-qt\${QT_VERSION_MAJOR} 1.0.0 REQUIRED)/g' CMakeLists.txt
sed -i 's/, KDSingleApplication::Option::IncludeUsernameInSocketName);$/);/g' src/main.cpp
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
@@ -733,11 +676,11 @@ jobs:
submodules: recursive
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.1.9
uses: vmactions/freebsd-vm@v1.1.8
with:
usesh: true
mem: 4096
prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio sparsehash rapidjson
prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio
run: |
set -e
git config --global --add safe.directory ${GITHUB_WORKSPACE}
@@ -762,13 +705,13 @@ 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
run: |
set -e
export LDFLAGS="-L/usr/local/lib"
git config --global --add safe.directory ${GITHUB_WORKSPACE}
cmake -E make_directory build
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug" -DENABLE_ALSA=OFF -DENABLE_DISCORD_RPC=OFF
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug" -DENABLE_ALSA=OFF
cmake --build build --config Debug --parallel 4
@@ -779,7 +722,7 @@ jobs:
strategy:
fail-fast: false
matrix:
runner: [ 'macos-13', 'macos-15' ]
runner: [ 'macos-13', 'macos-14' ]
buildtype: [ 'release' ]
runs-on: ${{ matrix.runner }}
@@ -788,7 +731,7 @@ jobs:
- name: Set MACOSX_DEPLOYMENT_TARGET
run: |
for i in 13 14 15; do
for i in 12 13 14 15; do
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
echo "Using macOS SDK ${i}"
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
@@ -825,7 +768,7 @@ jobs:
- name: Import certificate file
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false
uses: apple-actions/import-codesign-certs@v5
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
@@ -887,7 +830,7 @@ jobs:
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-14'
working-directory: build
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
@@ -946,7 +889,7 @@ jobs:
- name: Set MACOSX_DEPLOYMENT_TARGET
run: |
for i in 13 14 15; do
for i in 12 13 14 15; do
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
echo "Using macOS SDK ${i}"
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV

4
.gitmodules vendored
View File

@@ -0,0 +1,4 @@
[submodule "3rdparty/kdsingleapplication/KDSingleApplication"]
path = 3rdparty/kdsingleapplication/KDSingleApplication
url = https://github.com/KDAB/KDSingleApplication.git
branch = master

11
3rdparty/README.md vendored Normal file
View File

@@ -0,0 +1,11 @@
3rdparty libraries located in this directory
============================================
KDSingleApplication
-------------------
A small library used by Strawberry to prevent it from starting twice per user session.
If the user tries to start strawberry twice, the main window will maximize instead of starting another instance.
It is also used to pass command-line options through to the first instance.
This 3rdparty copy is used only if KDSingleApplication 1.1 or higher is not found on the system.
URL: https://github.com/KDAB/KDSingleApplication/

View File

@@ -1 +0,0 @@
add_subdirectory(src)

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,12 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void Discord_Register(const char* applicationId, const char* command);
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId);
#ifdef __cplusplus
}
#endif

View File

@@ -1,77 +0,0 @@
#pragma once
#include <stdint.h>
// clang-format off
// clang-format on
namespace discord_rpc {
#ifdef __cplusplus
extern "C" {
#endif
typedef struct DiscordRichPresence {
int 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,
int autoRegister,
const char* optionalSteamId);
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" */
} // namespace discord_rpc
#endif

View File

@@ -1,41 +0,0 @@
set(DISCORD_RPC_SOURCES
../include/discord_rpc.h
../include/discord_register.h
discord_rpc.cpp
rpc_connection.h
rpc_connection.cpp
serialization.h
serialization.cpp
connection.h
backoff.h
msg_queue.h
)
if(UNIX)
list(APPEND DISCORD_RPC_SOURCES 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 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}/../include)

View File

@@ -1,44 +0,0 @@
#pragma once
#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

View File

@@ -1,22 +0,0 @@
#pragma once
// 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

View File

@@ -1,122 +0,0 @@
#include "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;
}
/*static*/ BaseConnection *BaseConnection::Create() {
PipeAddr.sun_family = AF_UNIX;
return &Connection;
}
/*static*/ 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,126 +0,0 @@
#include "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;
/*static*/ BaseConnection *BaseConnection::Create() {
return &Connection;
}
/*static*/ 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,104 +0,0 @@
#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]) {
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");
}
}
extern "C" void Discord_RegisterSteamGame(const char *applicationId,
const char *steamId) {
char command[256];
sprintf(command, "xdg-open steam://rungameid/%s", steamId);
Discord_Register(applicationId, command);
}

View File

@@ -1,80 +0,0 @@
#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);
}
}
}
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
{
char command[256];
snprintf(command, 256, "steam://rungameid/%s", steamId);
Discord_Register(applicationId, command);
}

View File

@@ -1,183 +0,0 @@
#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);
}
extern "C" void Discord_RegisterSteamGame(const char *applicationId,
const char *steamId) {
wchar_t appId[32];
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
wchar_t wSteamId[32];
MultiByteToWideChar(CP_UTF8, 0, steamId, -1, wSteamId, 32);
HKEY key;
auto status = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", 0, KEY_READ, &key);
if (status != ERROR_SUCCESS) {
fprintf(stderr, "Error opening Steam key\n");
return;
}
wchar_t steamPath[MAX_PATH];
DWORD pathBytes = sizeof(steamPath);
status = RegQueryValueExW(key, L"SteamExe", nullptr, nullptr, (BYTE *)steamPath, &pathBytes);
RegCloseKey(key);
if (status != ERROR_SUCCESS || pathBytes < 1) {
fprintf(stderr, "Error reading SteamExe key\n");
return;
}
DWORD pathChars = pathBytes / sizeof(wchar_t);
for (DWORD i = 0; i < pathChars; ++i) {
if (steamPath[i] == L'/') {
steamPath[i] = L'\\';
}
}
wchar_t command[1024];
StringCbPrintfW(command, sizeof(command), L"\"%s\" steam://rungameid/%s", steamPath, wSteamId);
Discord_RegisterW(appId, command);
}

View File

@@ -1,479 +0,0 @@
#include "discord_rpc.h"
#include "backoff.h"
#include "discord_register.h"
#include "msg_queue.h"
#include "rpc_connection.h"
#include "serialization.h"
#include <atomic>
#include <chrono>
#include <mutex>
#include <condition_variable>
#include <thread>
namespace discord_rpc {
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 };
static void Discord_UpdateConnection(void);
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 Discord_UpdateConnection(void) {
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();
}
}
}
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;
}
extern "C" void Discord_Initialize(const char *applicationId,
DiscordEventHandlers *handlers,
int autoRegister,
const char *optionalSteamId) {
IoThread = new (std::nothrow) IoThreadHolder();
if (IoThread == nullptr) {
return;
}
if (autoRegister) {
if (optionalSteamId && optionalSteamId[0]) {
Discord_RegisterSteamGame(applicationId, optionalSteamId);
}
else {
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(void) {
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(void) {
// 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;
}
bool wasDisconnected = WasJustDisconnected.exchange(false);
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()) {
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 = {};
}
return;
}
} // namespace discord_rpc

View File

@@ -1,40 +0,0 @@
#pragma once
#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

View File

@@ -1,133 +0,0 @@
#include "rpc_connection.h"
#include "serialization.h"
namespace discord_rpc {
static const int RpcVersion = 1;
static RpcConnection Instance;
/*static*/ RpcConnection *RpcConnection::Create(const char *applicationId) {
Instance.connection = BaseConnection::Create();
StringCopy(Instance.appId, applicationId);
return &Instance;
}
/*static*/ 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,64 +0,0 @@
#pragma once
#include "connection.h"
#include "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

View File

@@ -1,249 +0,0 @@
#include "serialization.h"
#include "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, int nonce) {
WriteKey(writer, "nonce");
char nonceBuffer[32];
NumberToString(nonceBuffer, nonce);
writer.String(nonceBuffer);
}
size_t JsonWriteRichPresenceObj(char *dest,
size_t maxLen,
int nonce,
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);
}
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, int reply, 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,183 +0,0 @@
#pragma once
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
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
struct DiscordRichPresence;
size_t JsonWriteRichPresenceObj(char *dest,
size_t maxLen,
int nonce,
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

View File

@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.13)
set(SOURCES KDSingleApplication/src/kdsingleapplication.cpp KDSingleApplication/src/kdsingleapplication_localsocket.cpp)
set(HEADERS KDSingleApplication/src/kdsingleapplication.h KDSingleApplication/src/kdsingleapplication_localsocket_p.h)
qt_wrap_cpp(MOC ${HEADERS})
add_library(kdsingleapplication STATIC ${SOURCES} ${MOC})
if(NOT MSVC)
target_compile_options(kdsingleapplication PRIVATE -Wno-missing-declarations)
endif()
target_compile_definitions(kdsingleapplication PRIVATE -DKDSINGLEAPPLICATION_STATIC_BUILD)
target_include_directories(kdsingleapplication PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(kdsingleapplication PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network)

File diff suppressed because it is too large Load Diff

View File

@@ -2,37 +2,6 @@ Strawberry Music Player
=======================
ChangeLog
Version 1.2.8 (2025.04.05):
Bugfixes:
* Added "HI_RES_LOSSLESS" for Tidal quality setting.
* Increased backend settings device lineedit height.
* Possible fix for KGlobalAccel shortcuts sometimes not working.
Enhancements:
* Removed deprecated Tidal username/password login.
* Turned off "Grey out unavailable songs in playlists on startup" by default.
* Added support for reading tags from streams.
* Added tooltips in equalizer, backend and appearance settings.
* Added full tag support for AIFF including embedded covers.
* Use card ID instead of index for ALSA devices.
* Removed KDSingleApplication from 3rdparty.
* Support arbitrarily large EBU R 128 loudness normalization.
New features:
* Added Discord rich presence support.
Version 1.2.7 (2025.01.31):
Bugfixes:
* Fixed strawberry exiting when clicking tray icon.
* Fixed Clementine import script errors.
* Disabled OSD Pretty on Wayland since it's not working properly.
Enhancements:
* Only maximize error dialog if Strawberry is the active window (#1627).
* Added QPA Platform Native Interface as optional component.
Version 1.2.6 (2025.01.17):
Bugfixes:

View File

@@ -4,7 +4,7 @@
[![Patreon](https://img.shields.io/badge/patreon-donate-green.svg)](https://patreon.com/jonaskvinge)
[![PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/jonaskvinge)
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt framework.
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt toolkit.
![Browse](https://raw.githubusercontent.com/strawberrymusicplayer/strawberry/master/data/screenshot/screenshot.png)
@@ -59,9 +59,7 @@ Funding developers is a way to contribute to open source projects you appreciate
* Audio equalizer
* Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
* Streaming from Subsonic compatible servers
* Unofficial Tidal, Spotify and Qobuz integration
* Discord rich presence
* Subsonic, Tidal, Spotify and Qobuz streaming support
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
@@ -72,7 +70,7 @@ It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
To build Strawberry from source you need the following installed on your system with the additional development packages/headers:
* [CMake 3.13 or higher](https://cmake.org/)
* [CMake](https://cmake.org/)
* C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/) or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
* [Boost](https://www.boost.org/)
@@ -83,7 +81,7 @@ To build Strawberry from source you need the following installed on your system
* [GStreamer](https://gstreamer.freedesktop.org/)
* [TagLib 1.12 or higher](https://www.taglib.org/)
* [ICU](https://unicode-org.github.io/icu/)
* [KDSingleApplication 1.1.0 or higher](https://github.com/KDAB/KDSingleApplication)
* [KDSingleApplication](https://github.com/KDAB/KDSingleApplication)
Optional dependencies:
@@ -94,24 +92,25 @@ Optional dependencies:
* MTP devices: [libmtp](http://libmtp.sourceforge.net/)
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
* Discord rich presence [RapidJSON](https://rapidjson.org/)
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.
### :wrench: Build from source
### :wrench: Compiling from source
### Get the code:
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
### Build and install:
### Compile and install:
cd strawberry
cmake -S . -B build
cmake --build build --parallel $(nproc)
sudo cmake --install build
mkdir build
cd build
cmake ..
make -j $(nproc)
sudo make install
To build on Windows with Visual Studio 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
To compile on Windows with Visual Studio 2019 or 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
### :penguin: Packaging status

View File

@@ -1,9 +1,9 @@
set(STRAWBERRY_VERSION_MAJOR 1)
set(STRAWBERRY_VERSION_MINOR 2)
set(STRAWBERRY_VERSION_PATCH 8)
set(STRAWBERRY_VERSION_PATCH 6)
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION OFF)
set(INCLUDE_GIT_REVISION ON)
set(majorminorpatch "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}.${STRAWBERRY_VERSION_PATCH}")

7
debian/control vendored
View File

@@ -15,14 +15,11 @@ Build-Depends: debhelper-compat (= 12),
libpulse-dev,
libtag1-dev,
libicu-dev,
libxkbcommon-dev,
qt6-base-dev,
qt6-base-private-dev,
qt6-base-dev-tools,
qt6-tools-dev,
qt6-tools-dev-tools,
qt6-l10n-tools,
libkdsingleapplication-qt6-dev,
libgstreamer1.0-dev,
libgstreamer-plugins-base1.0-dev,
libcdio-dev,
@@ -30,9 +27,7 @@ Build-Depends: debhelper-compat (= 12),
libmtp-dev,
libchromaprint-dev,
libfftw3-dev,
libebur128-dev,
libsparsehash-dev,
rapidjson-dev
libebur128-dev
Standards-Version: 4.7.0
Package: strawberry

4
debian/rules vendored
View File

@@ -3,10 +3,6 @@
export DH_VERBOSE=1
export DEB_BUILD_MAINT_OPTIONS=hardening=+all
override_dh_auto_configure:
dh_auto_configure -- \
-DBUILD_WERROR=ON
override_dh_installchangelogs:
dh_installchangelogs Changelog

View File

@@ -2,7 +2,7 @@
# Strawberry Music Player
# Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
# Copyright 2021, Alexey Vazhnov
# 2021 Alexey Vazhnov
#
# Strawberry is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,7 +19,7 @@
# SPDX-License-Identifier: GPL-3.0-only
# Based on https://wiki.strawberrymusicplayer.org/wiki/Import_collection_library_and_playlists_from_Clementine
# Based on https://github.com/strawberrymusicplayer/strawberry/wiki/Import-collection-library-and-playlists-data-from-Clementine
set -o nounset
set -o errexit
@@ -35,8 +35,8 @@ test -f "$FILE_DST" || { echo "No such file: $FILE_DST"; exit 1; }
echo "Will try to copy information from $FILE_SRC to $FILE_DST."
echo
echo 'This script will **delete all data** from the Strawberry database!'
read -r -p 'Do you want to continue? (Only YES is accepted) ' answer
echo 'This script will **delete all information** from Strawberry database!'
read -r -p 'Do you want to continue? (the only YES is accepted) ' answer
if [ "$answer" != "YES" ]; then exit 1; fi
# 'heredoc' with substitution of variables, see `man bash`, "Here Documents":
@@ -62,9 +62,9 @@ INSERT INTO strawberry.subdirectories (directory_id, path, mtime) SELECT directo
INSERT INTO strawberry.songs (ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory_id, url, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, compilation_detected, compilation_on, compilation_off, compilation_effective, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating)
SELECT ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory, filename, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, sampler, forced_compilation_on, forced_compilation_off, effective_compilation, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating FROM clementine.songs WHERE unavailable = 0;
UPDATE strawberry.songs SET source = 2;
UPDATE strawberry.songs SET artist_id = '';
UPDATE strawberry.songs SET album_id = '';
UPDATE strawberry.songs SET song_id = '';
UPDATE strawberry.songs SET artist_id = "";
UPDATE strawberry.songs SET album_id = "";
UPDATE strawberry.songs SET song_id = "";
/* Import playlists */
@@ -140,7 +140,7 @@ SELECT ROWID,
bitrate,
samplerate,
directory,
CASE WHEN filename IS NULL THEN '' ELSE filename END,
filename,
filetype,
filesize,
mtime,
@@ -162,9 +162,16 @@ SELECT ROWID,
UPDATE strawberry.playlist_items SET source = 2;
UPDATE strawberry.playlist_items SET type = 2;
UPDATE strawberry.playlist_items SET artist_id = '';
UPDATE strawberry.playlist_items SET album_id = '';
UPDATE strawberry.playlist_items SET song_id = '';
UPDATE strawberry.playlist_items SET artist_id = "";
UPDATE strawberry.playlist_items SET album_id = "";
UPDATE strawberry.playlist_items SET song_id = "";
/* Recreate the FTS tables */
DELETE FROM strawberry.songs_fts;
INSERT INTO strawberry.songs_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment
FROM strawberry.songs;
EOF

View File

@@ -51,8 +51,6 @@
</screenshots>
<update_contact>eclipseo@fedoraproject.org</update_contact>
<releases>
<release version="1.2.8" date="2025-04-05"/>
<release version="1.2.7" date="2025-01-31"/>
<release version="1.2.6" date="2025-01-17"/>
<release version="1.2.5" date="2025-01-17"/>
<release version="1.2.4" date="2025-01-10"/>

View File

@@ -63,14 +63,8 @@ BuildRequires: pkgconfig(libcdio)
BuildRequires: pkgconfig(libebur128)
BuildRequires: pkgconfig(libgpod-1.0)
BuildRequires: pkgconfig(libmtp)
BuildRequires: pkgconfig(libsparsehash)
BuildRequires: cmake(GTest)
BuildRequires: pkgconfig(gmock)
BuildRequires: cmake(RapidJSON)
%if 0%{?fedora} || (0%{?suse_version} && 0%{?suse_version} > 1600) || "%{?_vendor}" == "openmandriva"
BuildRequires: cmake(KDSingleApplication-qt6)
%endif
%if 0%{?suse_version}
Requires: qt6-sql-sqlite
@@ -102,7 +96,7 @@ Features:
- Streaming support for Subsonic-compatible servers
- Unofficial streaming support for Tidal and Qobuz
%if 0%{?suse_version} && 0%{?suse_version} <= 1600
%if 0%{?suse_version}
%debug_package
%endif

View File

@@ -259,7 +259,7 @@ Section "Strawberry" Strawberry
File "libssl-3-x64.dll"
!endif
File "libFLAC-14.dll"
File "libFLAC-12.dll"
File "libbrotlicommon.dll"
File "libbrotlidec.dll"
File "libbrotlienc.dll"
@@ -324,7 +324,7 @@ Section "Strawberry" Strawberry
File "libqtsparkle-qt6.dll"
File "libsoup-3.0-0.dll"
File "libspeex-1.dll"
File "libsqlite3.dll"
File "libsqlite3-0.dll"
File "libssp-0.dll"
File "libstdc++-6.dll"
File "libtag.dll"
@@ -458,11 +458,11 @@ Section "Strawberry" Strawberry
; Common files
File "icudt77.dll"
File "icudt76.dll"
File "libfftw3-3.dll"
!ifdef msvc && debug
File "icuin77d.dll"
File "icuuc77d.dll"
File "icuin76d.dll"
File "icuuc76d.dll"
File "libxml2d.dll"
File "Qt6Concurrentd.dll"
File "Qt6Cored.dll"
@@ -471,8 +471,8 @@ Section "Strawberry" Strawberry
File "Qt6Sqld.dll"
File "Qt6Widgetsd.dll"
!else
File "icuin77.dll"
File "icuuc77.dll"
File "icuin76.dll"
File "icuuc76.dll"
File "libxml2.dll"
File "Qt6Concurrent.dll"
File "Qt6Core.dll"
@@ -596,7 +596,6 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=libgstapp.dll" "gstreamer-plugins\libgstapp.dll"
File "/oname=libgstasf.dll" "gstreamer-plugins\libgstasf.dll"
File "/oname=libgstasfmux.dll" "gstreamer-plugins\libgstasfmux.dll"
File "/oname=libgstasio.dll" "gstreamer-plugins\libgstasio.dll"
File "/oname=libgstaudioconvert.dll" "gstreamer-plugins\libgstaudioconvert.dll"
File "/oname=libgstaudiofx.dll" "gstreamer-plugins\libgstaudiofx.dll"
File "/oname=libgstaudioparsers.dll" "gstreamer-plugins\libgstaudioparsers.dll"
@@ -720,7 +719,7 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll"
!ifdef arch_x64
;File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
!endif
!endif ; MSVC
@@ -784,7 +783,7 @@ Section "Uninstall"
Delete "$INSTDIR\libssl-3-x64.dll"
!endif
Delete "$INSTDIR\libFLAC-14.dll"
Delete "$INSTDIR\libFLAC-12.dll"
Delete "$INSTDIR\libbrotlicommon.dll"
Delete "$INSTDIR\libbrotlidec.dll"
Delete "$INSTDIR\libbrotlienc.dll"
@@ -849,7 +848,7 @@ Section "Uninstall"
Delete "$INSTDIR\libqtsparkle-qt6.dll"
Delete "$INSTDIR\libsoup-3.0-0.dll"
Delete "$INSTDIR\libspeex-1.dll"
Delete "$INSTDIR\libsqlite3.dll"
Delete "$INSTDIR\libsqlite3-0.dll"
Delete "$INSTDIR\libssp-0.dll"
Delete "$INSTDIR\libstdc++-6.dll"
Delete "$INSTDIR\libtag.dll"
@@ -982,11 +981,11 @@ Section "Uninstall"
; Common files
Delete "$INSTDIR\icudt77.dll"
Delete "$INSTDIR\icudt76.dll"
Delete "$INSTDIR\libfftw3-3.dll"
!ifdef msvc && debug
Delete "$INSTDIR\icuin77d.dll"
Delete "$INSTDIR\icuuc77d.dll"
Delete "$INSTDIR\icuin76d.dll"
Delete "$INSTDIR\icuuc76d.dll"
Delete "$INSTDIR\libxml2d.dll"
Delete "$INSTDIR\Qt6Concurrentd.dll"
Delete "$INSTDIR\Qt6Cored.dll"
@@ -995,8 +994,8 @@ Section "Uninstall"
Delete "$INSTDIR\Qt6Sqld.dll"
Delete "$INSTDIR\Qt6Widgetsd.dll"
!else
Delete "$INSTDIR\icuin77.dll"
Delete "$INSTDIR\icuuc77.dll"
Delete "$INSTDIR\icuin76.dll"
Delete "$INSTDIR\icuuc76.dll"
Delete "$INSTDIR\libxml2.dll"
Delete "$INSTDIR\Qt6Concurrent.dll"
Delete "$INSTDIR\Qt6Core.dll"
@@ -1053,7 +1052,6 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\libgstapp.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstasf.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstasfmux.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstasio.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudioconvert.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudiofx.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstaudioparsers.dll"
@@ -1179,7 +1177,7 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
!ifdef arch_x64
;Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
!endif
!endif ; msvc

View File

@@ -1,2 +1,71 @@
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
add_subdirectory(utilities)
add_subdirectory(core)
add_subdirectory(mimedata)
add_subdirectory(osd)
add_subdirectory(tagreader)
add_subdirectory(widgets)
add_subdirectory(dialogs)
add_subdirectory(engine)
add_subdirectory(lyrics)
add_subdirectory(filterparser)
add_subdirectory(analyzer)
add_subdirectory(transcoder)
add_subdirectory(collection)
add_subdirectory(playlist)
add_subdirectory(playlistparsers)
add_subdirectory(equalizer)
add_subdirectory(edittagdialog)
add_subdirectory(smartplaylists)
add_subdirectory(settings)
add_subdirectory(device)
add_subdirectory(covermanager)
add_subdirectory(fileview)
add_subdirectory(player)
add_subdirectory(radios)
add_subdirectory(streaming)
add_subdirectory(scrobbler)
add_subdirectory(organize)
add_subdirectory(context)
add_subdirectory(queue)
add_subdirectory(providers)
add_subdirectory(songloader)
add_subdirectory(systemtrayicon)
if(HAVE_MUSICBRAINZ)
add_subdirectory(musicbrainz)
endif()
if(HAVE_GLOBALSHORTCUTS)
add_subdirectory(globalshortcuts)
endif()
if(HAVE_MOODBAR)
add_subdirectory(moodbar)
endif()
if(HAVE_MPRIS2)
add_subdirectory(mpris2)
endif()
if(HAVE_SUBSONIC)
add_subdirectory(subsonic)
endif()
if(HAVE_TIDAL)
add_subdirectory(tidal)
endif()
if(HAVE_SPOTIFY)
add_subdirectory(spotify)
endif()
if(HAVE_QOBUZ)
add_subdirectory(qobuz)
endif()
if(APPLE)
add_subdirectory(macstartup)
endif()

View File

@@ -0,0 +1,41 @@
set(ANALYZER_SOURCES
fht.cpp
analyzerbase.cpp
analyzercontainer.cpp
blockanalyzer.cpp
boomanalyzer.cpp
turbineanalyzer.cpp
sonogramanalyzer.cpp
waverubberanalyzer.cpp
rainbowanalyzer.cpp
)
set(ANALYZER_HEADERS
analyzerbase.h
analyzercontainer.h
blockanalyzer.h
boomanalyzer.h
turbineanalyzer.h
sonogramanalyzer.h
waverubberanalyzer.h
rainbowanalyzer.h
)
qt_wrap_cpp(ANALYZER_SOURCES ${ANALYZER_HEADERS})
add_library(strawberry_analyzer STATIC ${ANALYZER_SOURCES})
target_include_directories(strawberry_analyzer PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/src
)
target_link_libraries(strawberry_analyzer PRIVATE
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
strawberry_core
strawberry_engine
)

View File

@@ -100,7 +100,7 @@ void AnalyzerBase::transform(Scope &scope) {
fht_->logSpectrum(scope.data(), aux.data());
fht_->scale(scope.data(), 1.0F / 20);
scope.resize(static_cast<size_t>(fht_->size() / 2)); // second half of values are rubbish
scope.resize(fht_->size() / 2); // second half of values are rubbish
}
@@ -112,7 +112,7 @@ void AnalyzerBase::paintEvent(QPaintEvent *e) {
switch (engine_->state()) {
case EngineBase::State::Playing:{
const EngineBase::Scope &thescope = engine_->scope(timeout_);
size_t i = 0;
int i = 0;
// convert to mono here - our built in analyzers need mono, but the engines provide interleaved pcm
for (uint x = 0; static_cast<int>(x) < fht_->size(); ++x) {
@@ -124,7 +124,7 @@ void AnalyzerBase::paintEvent(QPaintEvent *e) {
transform(lastscope_);
analyze(p, lastscope_, new_frame_);
lastscope_.resize(static_cast<size_t>(fht_->size()));
lastscope_.resize(fht_->size());
break;
}
@@ -153,7 +153,7 @@ int AnalyzerBase::resizeExponent(int exp) {
if (exp != fht_->sizeExp()) {
delete fht_;
fht_ = new FHT(static_cast<uint>(exp));
fht_ = new FHT(exp);
}
return exp;

View File

@@ -85,7 +85,7 @@ void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
// this is the y-offset for drawing from the top of the widget
y_ = (height() - (rows_ * (kHeight + 1)) + 2) / 2;
scope_.resize(static_cast<size_t>(columns_));
scope_.resize(columns_);
if (rows_ != oldRows) {
barpixmap_ = QPixmap(kWidth, rows_ * (kHeight + 1));
@@ -165,9 +165,9 @@ void BlockAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
// Paint the background
canvas_painter.drawPixmap(0, 0, background_);
for (qint64 x = 0, y = 0; x < static_cast<qint64>(scope_.size()); ++x) {
for (int x = 0, y = 0; x < static_cast<int>(scope_.size()); ++x) {
// determine y
for (y = 0; scope_[static_cast<quint64>(x)] < yscale_.at(y); ++y);
for (y = 0; scope_[x] < yscale_.at(y); ++y);
// This is opposite to what you'd think, higher than y means the bar is lower than y (physically)
if (static_cast<double>(y) > store_.at(x)) {
@@ -175,13 +175,13 @@ void BlockAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
y = static_cast<int>(store_.value(x));
}
else {
store_[x] = static_cast<double>(y);
store_[x] = y;
}
// If y is lower than fade_pos_, then the bar has exceeded the height of the fadeout
// if the fadeout is quite faded now, then display the new one
if (y <= fade_pos_.at(x) /*|| fade_intensity_[x] < kFadeSize / 3*/) {
fade_pos_[x] = static_cast<int>(y);
fade_pos_[x] = y;
fade_intensity_[x] = kFadeSize;
}
@@ -189,13 +189,13 @@ void BlockAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame) {
--fade_intensity_[x];
const int offset = fade_intensity_.value(x);
const int y2 = y_ + (fade_pos_.value(x) * (kHeight + 1));
canvas_painter.drawPixmap(static_cast<int>(x) * (kWidth + 1), y2, fade_bars_[offset], 0, 0, kWidth, height() - y2);
canvas_painter.drawPixmap(x * (kWidth + 1), y2, fade_bars_[offset], 0, 0, kWidth, height() - y2);
}
if (fade_intensity_.at(x) == 0) fade_pos_[x] = rows_;
// REMEMBER: y is a number from 0 to rows_, 0 means all blocks are glowing, rows_ means none are
canvas_painter.drawPixmap(static_cast<int>(x) * (kWidth + 1), static_cast<int>(y) * (kHeight + 1) + y_, *bar(), 0, static_cast<int>(y) * (kHeight + 1), bar()->width(), bar()->height());
canvas_painter.drawPixmap(x * (kWidth + 1), y * (kHeight + 1) + y_, *bar(), 0, y * (kHeight + 1), bar()->width(), bar()->height());
}
for (int x = 0; x < store_.size(); ++x) {

View File

@@ -76,7 +76,7 @@ void BoomAnalyzer::resizeEvent(QResizeEvent *e) {
const double h = 1.2 / HEIGHT;
bands_ = qMin(static_cast<int>(static_cast<double>(width() + 1) / (kColumnWidth + 1)) + 1, kMaxBandCount);
scope_.resize(static_cast<size_t>(bands_));
scope_.resize(bands_);
F_ = static_cast<double>(HEIGHT) / (log10(256) * 1.1 /*<- max. amplitude*/);
@@ -112,19 +112,17 @@ void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame
return;
}
const uint MAX_HEIGHT = static_cast<uint>(height() - 1);
const uint MAX_HEIGHT = height() - 1;
QPainter canvas_painter(&canvas_);
canvas_.fill(palette().color(QPalette::Window));
interpolate(scope, scope_);
int x = 0;
int y = 0;
for (size_t i = 0; i < static_cast<size_t>(bands_); ++i, x += kColumnWidth + 1) {
for (int i = 0, x = 0, y = 0; i < bands_; ++i, x += kColumnWidth + 1) {
double h = log10(scope_[i] * 256.0) * F_;
h = std::min(h, static_cast<double>(MAX_HEIGHT));
if (h > MAX_HEIGHT) h = MAX_HEIGHT;
if (h > bar_height_[i]) {
bar_height_[i] = h;
@@ -140,7 +138,7 @@ void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame
else {
if (bar_height_[i] > 0.0) {
bar_height_[i] -= K_barHeight_; // 1.4
bar_height_[i] = std::max(0.0, bar_height_[i]);
if (bar_height_[i] < 0.0) bar_height_[i] = 0.0;
}
peak_handling:
@@ -149,8 +147,8 @@ void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame
peak_height_[i] -= peak_speed_[i];
peak_speed_[i] *= F_peakSpeed_; // 1.12
peak_height_[i] = std::max(bar_height_[i], bar_height_[i]);
peak_height_[i] = std::max(0.0, peak_height_[i]);
if (peak_height_[i] < bar_height_[i]) peak_height_[i] = bar_height_[i];
if (peak_height_[i] < 0.0) peak_height_[i] = 0.0;
}
}

View File

@@ -129,7 +129,7 @@ void RainbowAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_frame)
// of band pass filters for this, so bands can leak into neighbouring bands,
// but for now it's a series of separate square filters.
const int samples_per_band = scope_size / kRainbowBands;
size_t sample = 0;
int sample = 0;
for (int band = 0; band < kRainbowBands; ++band) {
float accumulator = 0.0;
for (int i = 0; i < samples_per_band; ++i) {

View File

@@ -85,10 +85,10 @@ void SonogramAnalyzer::transform(Scope &scope) {
fht_->power2(scope.data());
fht_->scale(scope.data(), 1.0 / 256);
scope.resize(static_cast<size_t>(fht_->size() / 2));
scope.resize(fht_->size() / 2);
}
void SonogramAnalyzer::demo(QPainter &p) {
analyze(p, Scope(static_cast<size_t>(fht_->size()), 0), new_frame_);
analyze(p, Scope(fht_->size(), 0), new_frame_);
}

View File

@@ -43,7 +43,7 @@ void TurbineAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_fr
return;
}
const uint hd2 = static_cast<uint>(height() / 2);
const uint hd2 = height() / 2;
const uint kMaxHeight = hd2 - 1;
QPainter canvas_painter(&canvas_);
@@ -67,7 +67,7 @@ void TurbineAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_fr
else {
if (bar_height_[i] > 0.0) {
bar_height_[i] -= K_barHeight_; // 1.4
bar_height_[i] = std::max(0.0, bar_height_[i]);
if (bar_height_[i] < 0.0) bar_height_[i] = 0.0;
}
peak_handling:

View File

@@ -54,13 +54,13 @@ void WaveRubberAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_fra
const float *amplitude_data = s.data();
const int mid_y = height() / 4;
const size_t num_samples = static_cast<size_t>(s.size());
const int num_samples = static_cast<int>(s.size());
const float x_scale = static_cast<float>(width()) / static_cast<float>(num_samples);
float prev_y = static_cast<float>(mid_y);
// Draw the waveform
for (size_t i = 0; i < num_samples; ++i) {
for (int i = 0; i < num_samples; ++i) {
// Normalize amplitude to 0-1 range
const float color_factor = amplitude_data[i] / 2.0F + 0.5F;
@@ -88,5 +88,5 @@ void WaveRubberAnalyzer::transform(Scope &scope) {
}
void WaveRubberAnalyzer::demo(QPainter &p) {
analyze(p, Scope(static_cast<size_t>(fht_->size()), 0), new_frame_);
analyze(p, Scope(fht_->size(), 0), new_frame_);
}

View File

@@ -0,0 +1,82 @@
set(COLLECTION_SOURCES
collectionlibrary.cpp
collectionmodel.cpp
collectionbackend.cpp
collectionwatcher.cpp
collectionview.cpp
collectionitemdelegate.cpp
collectionviewcontainer.cpp
collectiondirectorymodel.cpp
collectionfilteroptions.cpp
collectionfilterwidget.cpp
collectionfilter.cpp
collectionplaylistitem.cpp
collectionquery.cpp
savedgroupingmanager.cpp
groupbydialog.cpp
collectiontask.cpp
collectionmodelupdate.cpp
collectionitem.cpp
)
set(COLLECTION_HEADERS
collectionlibrary.h
collectionmodel.h
collectionbackend.h
collectionwatcher.h
collectionview.h
collectionitemdelegate.h
collectionviewcontainer.h
collectiondirectorymodel.h
collectionfilterwidget.h
collectionfilter.h
savedgroupingmanager.h
groupbydialog.h
)
set(COLLECTION_UI
groupbydialog.ui
collectionfilterwidget.ui
collectionviewcontainer.ui
savedgroupingmanager.ui
)
qt_wrap_cpp(COLLECTION_SOURCES ${COLLECTION_HEADERS})
qt_wrap_ui(COLLECTION_SOURCES ${COLLECTION_UI})
add_library(strawberry_collection STATIC ${COLLECTION_SOURCES})
target_include_directories(strawberry_collection PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/src
)
target_link_libraries(strawberry_collection PRIVATE
PkgConfig::GLIB
PkgConfig::GOBJECT
PkgConfig::GSTREAMER
PkgConfig::GSTREAMER_BASE
PkgConfig::GSTREAMER_AUDIO
PkgConfig::GSTREAMER_APP
PkgConfig::GSTREAMER_TAG
PkgConfig::GSTREAMER_PBUTILS
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Sql
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
strawberry_utilities
strawberry_core
strawberry_mimedata
strawberry_engine
strawberry_tagreader
strawberry_covermanager
strawberry_filterparser
strawberry_dialogs
strawberry_edittagdialog
strawberry_organize
strawberry_playlistparsers
)

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -522,7 +522,7 @@ void CollectionBackend::SongPathChanged(const Song &song, const QFileInfo &new_f
updated_song.set_url(QUrl::fromLocalFile(QDir::cleanPath(new_file.filePath())));
updated_song.set_basefilename(new_file.fileName());
updated_song.InitArtManual();
if (updated_song.is_linked_collection_song() && new_collection_directory_id) {
if (updated_song.is_collection_song() && new_collection_directory_id) {
updated_song.set_directory_id(new_collection_directory_id.value());
}
@@ -853,10 +853,6 @@ void CollectionBackend::UpdateMTimesOnly(const SongList &songs) {
}
void CollectionBackend::DeleteSongsAsync(const SongList &songs) {
QMetaObject::invokeMethod(this, "DeleteSongs", Qt::QueuedConnection, Q_ARG(SongList, songs));
}
void CollectionBackend::DeleteSongs(const SongList &songs) {
QMutexLocker l(db_->Mutex());
@@ -883,24 +879,6 @@ void CollectionBackend::DeleteSongs(const SongList &songs) {
}
void CollectionBackend::DeleteSongsByUrlsAsync(const QList<QUrl> &urls) {
QMetaObject::invokeMethod(this, "DeleteSongsByUrl", Qt::QueuedConnection, Q_ARG(QList<QUrl>, urls));
}
void CollectionBackend::DeleteSongsByUrls(const QList<QUrl> &urls) {
SongList songs;
songs.reserve(urls.count());
for (const QUrl &url : urls) {
songs << GetSongsByUrl(url);
}
if (!songs.isEmpty()) {
DeleteSongs(songs);
}
}
void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool unavailable) {
QMutexLocker l(db_->Mutex());

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -234,9 +234,6 @@ class CollectionBackend : public CollectionBackendInterface {
void UpdateSongRatingAsync(const int id, const float rating, const bool save_tags = false);
void UpdateSongsRatingAsync(const QList<int> &ids, const float rating, const bool save_tags = false);
void DeleteSongsAsync(const SongList &songs);
void DeleteSongsByUrlsAsync(const QList<QUrl> &url);
public Q_SLOTS:
void Exit();
void GetAllSongs(const int id);
@@ -250,7 +247,6 @@ class CollectionBackend : public CollectionBackendInterface {
void UpdateSongsBySongID(const SongMap &new_songs);
void UpdateMTimesOnly(const SongList &songs);
void DeleteSongs(const SongList &songs);
void DeleteSongsByUrls(const QList<QUrl> &url);
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
void CompilationsNeedUpdating();

View File

@@ -28,7 +28,7 @@
#include <QUrl>
#include "core/song.h"
#include "core/songmimedata.h"
#include "mimedata/songmimedata.h"
#include "filterparser/filterparser.h"
#include "filterparser/filtertree.h"
#include "collectionbackend.h"

View File

@@ -53,7 +53,7 @@
#include "savedgroupingmanager.h"
#include "collectionfilterwidget.h"
#include "groupbydialog.h"
#include "ui_collectionfilterwidget.h"
#include "collection/ui_collectionfilterwidget.h"
#include "widgets/searchfield.h"
#include "constants/collectionsettings.h"
#include "constants/appearancesettings.h"

View File

@@ -199,8 +199,8 @@ void CollectionLibrary::SyncPlaycountAndRatingToFiles() {
task_manager_->SetTaskBlocksCollectionScans(task_id);
const SongList songs = backend_->GetAllSongs();
const quint64 nb_songs = static_cast<quint64>(songs.size());
quint64 i = 0;
const qint64 nb_songs = songs.size();
int i = 0;
for (const Song &song : songs) {
(void)tagreader_client_->SaveSongPlaycountBlocking(song.url().toLocalFile(), song.playcount());
(void)tagreader_client_->SaveSongRatingBlocking(song.url().toLocalFile(), song.rating());

View File

@@ -59,7 +59,7 @@
#include "core/database.h"
#include "core/iconloader.h"
#include "core/settings.h"
#include "core/songmimedata.h"
#include "mimedata/songmimedata.h"
#include "collectionfilteroptions.h"
#include "collectionquery.h"
#include "collectionbackend.h"
@@ -397,13 +397,13 @@ QMimeData *CollectionModel::mimeData(const QModelIndexList &indexes) const {
GetChildSongs(IndexToItem(idx), songs, song_ids, urls);
}
SongMimeData *song_mime_data = new SongMimeData;
song_mime_data->setUrls(urls);
song_mime_data->backend = backend_;
song_mime_data->songs = songs;
song_mime_data->name_for_new_playlist_ = Song::GetNameForNewPlaylist(songs);
SongMimeData *data = new SongMimeData;
data->setUrls(urls);
data->backend = backend_;
data->songs = songs;
data->name_for_new_playlist_ = Song::GetNameForNewPlaylist(data->songs);
return song_mime_data;
return data;
}

View File

@@ -155,7 +155,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
int total_artist_count() const { return total_artist_count_; }
int total_album_count() const { return total_album_count_; }
quint64 icon_disk_cache_size() { return static_cast<quint64>(icon_disk_cache_->cacheSize()); }
quint64 icon_disk_cache_size() { return icon_disk_cache_->cacheSize(); }
const CollectionModel::Grouping GetGroupBy() const { return options_current_.group_by; }
void SetGroupBy(const CollectionModel::Grouping g, const std::optional<bool> separate_albums_by_grouping = std::optional<bool>());

View File

@@ -1,6 +1,8 @@
/*
* Strawberry Music Player
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, 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
@@ -28,52 +30,41 @@
class SqlRow;
CollectionPlaylistItem::CollectionPlaylistItem(const Song::Source source) : PlaylistItem(source) {
song_.set_source(source);
CollectionPlaylistItem::CollectionPlaylistItem() : PlaylistItem(Song::Source::Collection) {
song_.set_source(Song::Source::Collection);
}
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(Song::Source::Collection), song_(song) {
song_.set_source(Song::Source::Collection);
}
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
void CollectionPlaylistItem::Reload() {
const TagReaderResult result = TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
if (!result.success()) {
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
return;
}
UpdateTemporaryMetadata(song_);
}
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
int col = 0;
switch (source_) {
case Song::Source::Collection:
col = 0;
break;
default:
col = static_cast<int>(Song::kRowIdColumns.count());
break;
}
song_.InitFromQuery(query, true, col);
// Rows from the songs tables come first
song_.InitFromQuery(query, true);
song_.set_source(Song::Source::Collection);
return song_.is_valid();
}
void CollectionPlaylistItem::Reload() {
QVariant CollectionPlaylistItem::DatabaseValue(DatabaseColumn column) const {
if (song_.url().isLocalFile()) {
const TagReaderResult result = TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
if (!result.success()) {
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
return;
}
UpdateTemporaryMetadata(song_);
}
}
QVariant CollectionPlaylistItem::DatabaseValue(const DatabaseColumn database_column) const {
switch (database_column) {
case DatabaseColumn::CollectionId:
return song_.id();
default:
return PlaylistItem::DatabaseValue(database_column);
switch (column) {
case Column_CollectionId: return song_.id();
default: return PlaylistItem::DatabaseValue(column);
}
}

View File

@@ -1,6 +1,8 @@
/*
* Strawberry Music Player
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, 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
@@ -32,11 +34,9 @@ class SqlRow;
class CollectionPlaylistItem : public PlaylistItem {
public:
explicit CollectionPlaylistItem(const Song::Source source);
explicit CollectionPlaylistItem();
explicit CollectionPlaylistItem(const Song &song);
QUrl Url() const override;
bool InitFromQuery(const SqlRow &query) override;
void Reload() override;
@@ -44,13 +44,15 @@ class CollectionPlaylistItem : public PlaylistItem {
Song OriginalMetadata() const override { return song_; }
void SetMetadata(const Song &song) override { song_ = song; }
QUrl Url() const override;
bool IsLocalCollectionItem() const override { return true; }
void SetArtManual(const QUrl &cover_url) override;
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
protected:
QVariant DatabaseValue(const DatabaseColumn database_column) const override;
Song DatabaseSongMetadata() const override { return Song(source_); }
QVariant DatabaseValue(DatabaseColumn column) const override;
Song DatabaseSongMetadata() const override { return Song(Song::Source::Collection); }
protected:
Song song_;

View File

@@ -51,7 +51,7 @@
#include <QContextMenuEvent>
#include "core/iconloader.h"
#include "core/mimedata.h"
#include "mimedata/mimedata.h"
#include "core/musicstorage.h"
#include "core/deletefiles.h"
#include "core/settings.h"
@@ -65,11 +65,11 @@
#include "collectionitem.h"
#include "collectionitemdelegate.h"
#include "collectionview.h"
#ifndef Q_OS_WIN32
#ifndef Q_OS_WIN
# include "device/devicemanager.h"
# include "device/devicestatefiltermodel.h"
#endif
#include "dialogs/edittagdialog.h"
#include "edittagdialog/edittagdialog.h"
#include "dialogs/deleteconfirmationdialog.h"
#include "organize/organizedialog.h"
#include "organize/organizeerrordialog.h"
@@ -95,7 +95,7 @@ CollectionView::CollectionView(QWidget *parent)
action_open_in_new_playlist_(nullptr),
action_organize_(nullptr),
action_search_for_this_(nullptr),
#ifndef Q_OS_WIN32
#ifndef Q_OS_WIN
action_copy_to_device_(nullptr),
#endif
action_edit_track_(nullptr),
@@ -417,7 +417,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
context_menu_->addSeparator();
action_organize_ = context_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &CollectionView::Organize);
#ifndef Q_OS_WIN32
#ifndef Q_OS_WIN
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &CollectionView::CopyToDevice);
#endif
action_delete_files_ = context_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &CollectionView::Delete);
@@ -439,7 +439,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
context_menu_->addMenu(filter_widget_->menu());
#ifndef Q_OS_WIN32
#ifndef Q_OS_WIN
action_copy_to_device_->setDisabled(device_manager_->connected_devices_model()->rowCount() == 0);
QObject::connect(device_manager_->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, action_copy_to_device_, &QAction::setDisabled);
#endif
@@ -481,7 +481,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
action_rescan_songs_->setEnabled(regular_editable > 0);
action_organize_->setVisible(regular_elements == regular_editable);
#ifndef Q_OS_WIN32
#ifndef Q_OS_WIN
action_copy_to_device_->setVisible(regular_elements == regular_editable);
#endif
@@ -492,7 +492,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
// only when all selected items are editable
action_organize_->setEnabled(regular_elements == regular_editable);
#ifndef Q_OS_WIN32
#ifndef Q_OS_WIN
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
#endif
@@ -759,7 +759,7 @@ void CollectionView::RescanSongs() {
void CollectionView::CopyToDevice() {
#ifndef Q_OS_WIN32
#ifndef Q_OS_WIN
if (!organize_dialog_) {
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
}

View File

@@ -176,7 +176,7 @@ class CollectionView : public AutoExpandingTreeView {
QAction *action_organize_;
QAction *action_search_for_this_;
#ifndef Q_OS_WIN32
#ifndef Q_OS_WIN
QAction *action_copy_to_device_;
#endif
QAction *action_edit_track_;

View File

@@ -26,7 +26,7 @@
#include "collectionfilterwidget.h"
#include "collectionview.h"
#include "collectionviewcontainer.h"
#include "ui_collectionviewcontainer.h"
#include "collection/ui_collectionviewcontainer.h"
CollectionViewContainer::CollectionViewContainer(QWidget *parent) : QWidget(parent), ui_(new Ui_CollectionViewContainer) {

View File

@@ -858,7 +858,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
QHash<quint64, Song> sections_map;
for (const Song &song : old_cue_songs) {
sections_map.insert(static_cast<quint64>(song.beginning_nanosec()), song);
sections_map.insert(song.beginning_nanosec(), song);
}
// Load new CUE songs
@@ -879,8 +879,8 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
PerformEBUR128Analysis(new_cue_song);
new_cue_song.set_fingerprint(fingerprint);
if (sections_map.contains(static_cast<quint64>(new_cue_song.beginning_nanosec()))) { // Changed section
const Song matching_cue_song = sections_map[static_cast<quint64>(new_cue_song.beginning_nanosec())];
if (sections_map.contains(new_cue_song.beginning_nanosec())) { // Changed section
const Song matching_cue_song = sections_map[new_cue_song.beginning_nanosec()];
new_cue_song.set_id(matching_cue_song.id());
new_cue_song.set_art_automatic(art_automatic);
new_cue_song.MergeUserSetData(matching_cue_song, true, true);
@@ -1082,7 +1082,7 @@ quint64 CollectionWatcher::GetMtimeForCue(const QString &cue_path) {
const QDateTime cue_last_modified = fileinfo.lastModified();
return cue_last_modified.isValid() ? static_cast<quint64>(cue_last_modified.toSecsSinceEpoch()) : 0;
return cue_last_modified.isValid() ? cue_last_modified.toSecsSinceEpoch() : 0;
}

View File

@@ -27,12 +27,10 @@
#cmakedefine HAVE_GLOBALSHORTCUTS
#cmakedefine HAVE_X11_GLOBALSHORTCUTS
#cmakedefine HAVE_KGLOBALACCEL_GLOBALSHORTCUTS
#cmakedefine HAVE_STREAMTAGREADER
#cmakedefine HAVE_SUBSONIC
#cmakedefine HAVE_TIDAL
#cmakedefine HAVE_SPOTIFY
#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_DISCORD_RPC
#cmakedefine HAVE_TAGLIB_DSFFILE
#cmakedefine HAVE_TAGLIB_DSDIFFFILE
@@ -43,8 +41,8 @@
#cmakedefine INSTALL_TRANSLATIONS
#define TRANSLATIONS_DIR "${CMAKE_INSTALL_PREFIX}/share/strawberry/translations"
#cmakedefine HAVE_QPA_QPLATFORMNATIVEINTERFACE
#cmakedefine HAVE_QX11APPLICATION
#cmakedefine HAVE_QPA_QPLATFORMNATIVEINTERFACE_H
#cmakedefine ENABLE_WIN32_CONSOLE

View File

@@ -26,12 +26,9 @@ namespace BackendSettings {
constexpr char kSettingsGroup[] = "Backend";
constexpr char kEngine[] = "engine";
constexpr char kEngineU[] = "Engine";
constexpr char kOutput[] = "output";
constexpr char kOutputU[] = "Output";
constexpr char kDevice[] = "device";
constexpr char kDeviceU[] = "Device";
constexpr char kEngine[] = "Engine";
constexpr char kOutput[] = "Output";
constexpr char kDevice[] = "Device";
constexpr char kALSAPlugin[] = "alsaplugin";
constexpr char kExclusiveMode[] = "exclusive_mode";
constexpr char kVolumeControl[] = "volume_control";

View File

@@ -65,12 +65,4 @@ constexpr QRgb kPresetRed = qRgb(202, 22, 16);
} // namespace
namespace DiscordRPCSettings {
constexpr char kSettingsGroup[] = "DiscordRPC";
constexpr char kEnabled[] = "enabled";
} // namespace
#endif // NOTIFICATIONSSETTINGS_H

View File

@@ -0,0 +1,32 @@
set(CONTEXT_SOURCES
contextview.cpp
contextalbum.cpp
)
set(CONTEXT_HEADERS
contextview.h
contextalbum.h
)
qt_wrap_cpp(CONTEXT_SOURCES ${CONTEXT_HEADERS})
add_library(strawberry_context STATIC ${CONTEXT_SOURCES})
target_include_directories(strawberry_context PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/src
)
target_link_libraries(strawberry_context PRIVATE
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Widgets
strawberry_utilities
strawberry_core
strawberry_collection
strawberry_covermanager
strawberry_lyrics
strawberry_widgets
)

95
src/core/CMakeLists.txt Normal file
View File

@@ -0,0 +1,95 @@
set(CORE_SOURCES
logging.cpp
commandlineoptions.cpp
database.cpp
memorydatabase.cpp
sqlquery.cpp
sqlrow.cpp
deletefiles.cpp
filesystemmusicstorage.cpp
filesystemwatcherinterface.cpp
mergedproxymodel.cpp
multisortfilterproxy.cpp
musicstorage.cpp
networkaccessmanager.cpp
threadsafenetworkdiskcache.cpp
networktimeouts.cpp
networkproxyfactory.cpp
qtfslistener.cpp
settings.cpp
settingsprovider.cpp
signalchecker.cpp
song.cpp
stylehelper.cpp
stylesheetloader.cpp
taskmanager.cpp
thread.cpp
urlhandlers.cpp
urlhandler.cpp
iconloader.cpp
standarditemiconloader.cpp
scopedtransaction.cpp
localredirectserver.cpp
temporaryfile.cpp
enginemetadata.cpp
platforminterface.cpp
)
set(CORE_HEADERS
logging.h
database.h
memorydatabase.h
deletefiles.h
filesystemwatcherinterface.h
mergedproxymodel.h
multisortfilterproxy.h
networkaccessmanager.h
threadsafenetworkdiskcache.h
networktimeouts.h
qtfslistener.h
settings.h
taskmanager.h
thread.h
urlhandlers.h
urlhandler.h
standarditemiconloader.h
stylesheetloader.h
localredirectserver.h
)
if(APPLE)
list(APPEND CORE_SOURCES scoped_nsautorelease_pool.mm)
endif()
if(WIN32)
list(APPEND CORE_SOURCES windows7thumbbar.cpp)
list(APPEND CORE_HEADERS windows7thumbbar.h)
endif()
qt_wrap_cpp(CORE_SOURCES ${CORE_HEADERS})
add_library(strawberry_core STATIC ${CORE_SOURCES})
target_include_directories(strawberry_core PRIVATE
${CMAKE_SOURCE_DIR}
${CMAKE_BINARY_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/src
)
target_link_libraries(strawberry_core PRIVATE
PkgConfig::GLIB
PkgConfig::GOBJECT
PkgConfig::SQLITE
${TAGLIB_LIBRARIES}
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Sql
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
$<$<BOOL:${WIN32}>:getopt-win::getopt>
strawberry_utilities
)

View File

@@ -598,7 +598,7 @@ void Database::BackupFile(const QString &filename) {
do {
ret = sqlite3_backup_step(backup, 16);
const int page_count = sqlite3_backup_pagecount(backup);
task_manager_->SetTaskProgress(task_id, static_cast<quint64>(page_count - sqlite3_backup_remaining(backup)), static_cast<quint64>(page_count));
task_manager_->SetTaskProgress(task_id, page_count - sqlite3_backup_remaining(backup), page_count);
}
while (ret == SQLITE_OK);

View File

@@ -92,7 +92,7 @@ void DeleteFiles::ProcessSomeFiles() {
// None left?
if (progress_ >= songs_.count()) {
task_manager_->SetTaskProgress(task_id_, static_cast<quint64>(progress_), static_cast<quint64>(songs_.count()));
task_manager_->SetTaskProgress(task_id_, progress_, songs_.count());
QString error_text;
storage_->FinishCopy(songs_with_errors_.isEmpty(), error_text);
@@ -114,7 +114,7 @@ void DeleteFiles::ProcessSomeFiles() {
const qint64 n = qMin(static_cast<qint64>(songs_.count()), static_cast<qint64>(progress_ + kBatchSize));
for (; progress_ < n; ++progress_) {
task_manager_->SetTaskProgress(task_id_, static_cast<quint64>(progress_), static_cast<quint64>(songs_.count()));
task_manager_->SetTaskProgress(task_id_, progress_, songs_.count());
const Song song = songs_.value(progress_);

View File

@@ -113,11 +113,7 @@ bool FilesystemMusicStorage::DeleteFromStorage(const DeleteJob &job) {
QString path = job.metadata_.url().toLocalFile();
QFileInfo fileInfo(path);
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
if (job.use_trash_ && QFile::supportsMoveToTrash()) {
#else
if (job.use_trash_) {
#endif
return QFile::moveToTrash(path);
}

View File

@@ -1,181 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 <QUrl>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QSslError>
#include <QJsonDocument>
#include <QJsonObject>
#include "core/logging.h"
#include "networkaccessmanager.h"
#include "httpbaserequest.h"
using namespace Qt::Literals::StringLiterals;
HttpBaseRequest::HttpBaseRequest(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: QObject(parent),
network_(network) {}
HttpBaseRequest::~HttpBaseRequest() {
if (!replies_.isEmpty()) {
qLog(Debug) << "Aborting" << replies_.count() << "network replies";
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
QObject::disconnect(reply, nullptr, this, nullptr);
reply->abort();
reply->deleteLater();
}
}
}
QNetworkReply *HttpBaseRequest::CreateGetRequest(const QUrl &url, const bool fake_user_agent_header) {
return CreateGetRequest(url, QUrlQuery(), fake_user_agent_header);
}
QNetworkReply *HttpBaseRequest::CreateGetRequest(const QUrl &url, const ParamList &params, const bool fake_user_agent_header) {
QUrlQuery url_query;
if (!params.isEmpty()) {
ParamList sorted_params = params;
std::sort(sorted_params.begin(), sorted_params.end());
for (const Param &param : sorted_params) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
}
return CreateGetRequest(url, url_query, fake_user_agent_header);
}
QNetworkReply *HttpBaseRequest::CreateGetRequest(const QUrl &url, const QUrlQuery &url_query, const bool fake_user_agent_header) {
QUrl request_url(url);
if (!url_query.isEmpty()) {
request_url.setQuery(url_query);
}
QNetworkRequest network_request(request_url);
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
if (use_authorization_header() && authenticated()) {
network_request.setRawHeader("Authorization", authorization_header());
}
if (fake_user_agent_header) {
network_request.setHeader(QNetworkRequest::UserAgentHeader, u"Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0"_s);
}
QNetworkReply *reply = network_->get(network_request);
QObject::connect(reply, &QNetworkReply::sslErrors, this, &HttpBaseRequest::HandleSSLErrors);
replies_ << reply;
//qLog(Debug) << service_name() << "Sending get request" << request_url;
return reply;
}
QNetworkReply *HttpBaseRequest::CreatePostRequest(const QUrl &url, const QByteArray &content_type_header, const QByteArray &data) {
QNetworkRequest network_request(url);
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
network_request.setHeader(QNetworkRequest::ContentTypeHeader, content_type_header);
if (use_authorization_header() && authenticated()) {
network_request.setRawHeader("Authorization", authorization_header());
}
QNetworkReply *reply = network_->post(network_request, data);
QObject::connect(reply, &QNetworkReply::sslErrors, this, &HttpBaseRequest::HandleSSLErrors);
replies_ << reply;
//qLog(Debug) << service_name() << "Sending post request" << url << data;
return reply;
}
QNetworkReply *HttpBaseRequest::CreatePostRequest(const QUrl &url, const QUrlQuery &url_query) {
return CreatePostRequest(url, "application/x-www-form-urlencoded", url_query.toString(QUrl::FullyEncoded).toUtf8());
}
QNetworkReply *HttpBaseRequest::CreatePostRequest(const QUrl &url, const ParamList &params) {
QUrlQuery url_query;
for (const Param &param : std::as_const(params)) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
return CreatePostRequest(url, url_query);
}
QNetworkReply *HttpBaseRequest::CreatePostRequest(const QUrl &url, const QJsonDocument &json_document) {
return CreatePostRequest(url, "application/json; charset=utf-8", json_document.toJson());
}
QNetworkReply *HttpBaseRequest::CreatePostRequest(const QUrl &url, const QJsonObject &json_object) {
return CreatePostRequest(url, QJsonDocument(json_object));
}
void HttpBaseRequest::HandleSSLErrors(const QList<QSslError> &ssl_errors) {
for (const QSslError &ssl_error : ssl_errors) {
Error(ssl_error.errorString());
}
}
HttpBaseRequest::ReplyDataResult HttpBaseRequest::GetReplyData(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError) {
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
const int http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (http_status_code < 200 || http_status_code > 207) {
return ReplyDataResult(ErrorCode::HttpError, QStringLiteral("Received HTTP code %1").arg(http_status_code));
}
}
return reply->readAll();
}
void HttpBaseRequest::Error(const QString &error_message, const QVariant &debug_output) {
qLog(Error) << service_name() << error_message;
if (debug_output.isValid()) {
qLog(Debug) << debug_output;
}
}

View File

@@ -1,110 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 HTTPBASEREQUEST_H
#define HTTPBASEREQUEST_H
#include <QObject>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QSslError>
#include <QJsonDocument>
#include <QJsonObject>
#include "includes/shared_ptr.h"
class NetworkAccessManager;
class HttpBaseRequest : public QObject {
Q_OBJECT
public:
explicit HttpBaseRequest(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
~HttpBaseRequest() override;
using Param = QPair<QString, QString>;
using ParamList = QList<Param>;
enum class ErrorCode {
Success,
NetworkError,
HttpError,
APIError,
ParseError,
};
class HttpBaseRequestResult {
public:
HttpBaseRequestResult(const ErrorCode _error_code, const QString &_error_message = QString())
: error_code(_error_code),
network_error(QNetworkReply::NetworkError::UnknownNetworkError),
http_status_code(200),
api_error(-1),
error_message(_error_message) {}
ErrorCode error_code;
QNetworkReply::NetworkError network_error;
int http_status_code;
int api_error;
QString error_message;
bool success() const { return error_code == ErrorCode::Success; }
};
class ReplyDataResult : public HttpBaseRequestResult {
public:
ReplyDataResult(const ErrorCode _error_code, const QString &_error_message = QString()) : HttpBaseRequestResult(_error_code, _error_message) {}
ReplyDataResult(const QByteArray &_data) : HttpBaseRequestResult(ErrorCode::Success), data(_data) {}
QByteArray data;
};
static ReplyDataResult GetReplyData(QNetworkReply *reply);
protected:
virtual QString service_name() const = 0;
virtual bool authentication_required() const = 0;
virtual bool authenticated() const = 0;
virtual bool use_authorization_header() const = 0;
virtual QByteArray authorization_header() const = 0;
virtual QNetworkReply *CreateGetRequest(const QUrl &url, const bool fake_user_agent_header);
virtual QNetworkReply *CreateGetRequest(const QUrl &url, const ParamList &params = ParamList(), const bool fake_user_agent_header = false);
virtual QNetworkReply *CreateGetRequest(const QUrl &url, const QUrlQuery &url_query, const bool fake_user_agent_header = false);
virtual QNetworkReply *CreatePostRequest(const QUrl &url, const QByteArray &content_type_header, const QByteArray &data);
virtual QNetworkReply *CreatePostRequest(const QUrl &url, const QUrlQuery &url_query);
virtual QNetworkReply *CreatePostRequest(const QUrl &url, const ParamList &params);
virtual QNetworkReply *CreatePostRequest(const QUrl &url, const QJsonDocument &json_document);
virtual QNetworkReply *CreatePostRequest(const QUrl &url, const QJsonObject &json_object);
virtual void Error(const QString &error_message, const QVariant &debug_output = QVariant());
public Q_SLOTS:
void HandleSSLErrors(const QList<QSslError> &ssl_errors);
Q_SIGNALS:
void ShowErrorDialog(const QString &error);
protected:
const SharedPtr<NetworkAccessManager> network_;
QList<QNetworkReply*> replies_;
};
#endif // HTTPBASEREQUEST_H

View File

@@ -43,7 +43,7 @@ bool IconLoader::custom_icons_ = false;
void IconLoader::Init() {
#if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN32)
#if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN)
Settings s;
s.beginGroup(AppearanceSettings::kSettingsGroup);
system_icons_ = s.value("system_icons", false).toBool();

View File

@@ -1,103 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 <QNetworkReply>
#include <QJsonDocument>
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonParseError>
#include "networkaccessmanager.h"
#include "jsonbaserequest.h"
using namespace Qt::Literals::StringLiterals;
JsonBaseRequest::JsonBaseRequest(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: HttpBaseRequest(network, parent) {}
JsonBaseRequest::JsonObjectResult JsonBaseRequest::GetJsonObject(const QByteArray &data) {
if (data.isEmpty()) {
return JsonObjectResult(ErrorCode::ParseError, "Empty data from server"_L1);
}
QJsonParseError json_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
if (json_error.error != QJsonParseError::NoError) {
return JsonObjectResult(ErrorCode::ParseError, json_error.errorString());
}
if (json_document.isEmpty()) {
return JsonObjectResult(ErrorCode::ParseError, "Received empty Json document."_L1);
}
if (!json_document.isObject()) {
return JsonObjectResult(ErrorCode::ParseError, "Json document is not an object."_L1);
}
const QJsonObject json_object = json_document.object();
if (json_object.isEmpty()) {
return JsonObjectResult(ErrorCode::ParseError, "Received empty Json object."_L1);
}
return json_object;
}
JsonBaseRequest::JsonValueResult JsonBaseRequest::GetJsonValue(const QJsonObject &json_object, const QString &name) {
if (!json_object.contains(name)) {
return JsonValueResult(ErrorCode::ParseError, QStringLiteral("Json object is missing value %1.").arg(name));
}
return json_object[name];
}
JsonBaseRequest::JsonObjectResult JsonBaseRequest::GetJsonObject(const QJsonObject &json_object, const QString &name) {
if (!json_object.contains(name)) {
return JsonValueResult(ErrorCode::ParseError, QStringLiteral("Json object is missing object %1.").arg(name));
}
const QJsonValue json_value = json_object[name];
if (!json_value.isObject()) {
return JsonValueResult(ErrorCode::ParseError, QStringLiteral("Json value %1 is not a object.").arg(name));
}
return json_value.toObject();
}
JsonBaseRequest::JsonArrayResult JsonBaseRequest::GetJsonArray(const QJsonObject &json_object, const QString &name) {
const JsonValueResult json_value_result = GetJsonValue(json_object, name);
if (!json_value_result.success()) {
return JsonArrayResult(ErrorCode::ParseError, json_value_result.error_message);
}
if (!json_value_result.json_value.isArray()) {
return JsonArrayResult(ErrorCode::ParseError, QStringLiteral("Json object value %1 is not a array.").arg(name));
}
return json_value_result.json_value.toArray();
}

View File

@@ -1,68 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 JSONBASEREQUEST_H
#define JSONBASEREQUEST_H
#include <QByteArray>
#include <QJsonObject>
#include <QJsonArray>
#include "includes/shared_ptr.h"
#include "httpbaserequest.h"
class NetworkAccessManager;
class JsonBaseRequest : public HttpBaseRequest {
Q_OBJECT
public:
explicit JsonBaseRequest(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
class JsonValueResult : public ReplyDataResult {
public:
JsonValueResult(const ReplyDataResult &reply_data_result) : ReplyDataResult(reply_data_result.error_code, reply_data_result.error_message) {}
JsonValueResult(const ErrorCode _error_code, const QString &_error_message = QString()) : ReplyDataResult(_error_code, _error_message) {}
JsonValueResult(const QJsonValue &_json_value) : ReplyDataResult(ErrorCode::Success), json_value(_json_value) {}
QJsonValue json_value;
};
class JsonObjectResult : public ReplyDataResult {
public:
JsonObjectResult(const ReplyDataResult &reply_data_result) : ReplyDataResult(reply_data_result.error_code, reply_data_result.error_message) {}
JsonObjectResult(const ErrorCode _error_code, const QString &_error_message = QString()) : ReplyDataResult(_error_code, _error_message) {}
JsonObjectResult(const QJsonObject &_json_object) : ReplyDataResult(ErrorCode::Success), json_object(_json_object) {}
QJsonObject json_object;
};
class JsonArrayResult : public ReplyDataResult {
public:
JsonArrayResult(const ReplyDataResult &reply_data_result) : ReplyDataResult(reply_data_result.error_code, reply_data_result.error_message) {}
JsonArrayResult(const ErrorCode _error_code, const QString &_error_message = QString()) : ReplyDataResult(_error_code, _error_message) {}
JsonArrayResult(const QJsonArray &_json_array) : ReplyDataResult(ErrorCode::Success), json_array(_json_array) {}
QJsonArray json_array;
};
static JsonObjectResult GetJsonObject(const QByteArray &data);
static JsonValueResult GetJsonValue(const QJsonObject &json_object, const QString &name);
static JsonObjectResult GetJsonObject(const QJsonObject &json_object, const QString &name);
static JsonArrayResult GetJsonArray(const QJsonObject &json_object, const QString &name);
};
#endif // JSONBASEREQUEST_H

View File

@@ -1,7 +1,8 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2018-2021, 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
@@ -43,8 +44,7 @@ using namespace Qt::Literals::StringLiterals;
LocalRedirectServer::LocalRedirectServer(QObject *parent)
: QTcpServer(parent),
port_(0),
socket_(nullptr),
success_(false) {}
socket_(nullptr) {}
LocalRedirectServer::~LocalRedirectServer() {
if (isListening()) close();
@@ -52,8 +52,7 @@ LocalRedirectServer::~LocalRedirectServer() {
bool LocalRedirectServer::Listen() {
if (!listen(QHostAddress::LocalHost, static_cast<quint64>(port_))) {
success_ = false;
if (!listen(QHostAddress::LocalHost, port_)) {
error_ = errorString();
return false;
}
@@ -62,7 +61,6 @@ bool LocalRedirectServer::Listen() {
url_.setHost(u"localhost"_s);
url_.setPort(serverPort());
url_.setPath(u"/"_s);
port_ = serverPort();
QObject::connect(this, &QTcpServer::newConnection, this, &LocalRedirectServer::NewConnection);
return true;
@@ -79,7 +77,7 @@ void LocalRedirectServer::NewConnection() {
void LocalRedirectServer::incomingConnection(qintptr socket_descriptor) {
if (socket_ != nullptr) {
if (socket_) {
if (socket_->state() == QAbstractSocket::ConnectedState) socket_->close();
socket_->deleteLater();
socket_ = nullptr;
@@ -90,7 +88,6 @@ void LocalRedirectServer::incomingConnection(qintptr socket_descriptor) {
if (!tcp_socket->setSocketDescriptor(socket_descriptor)) {
delete tcp_socket;
close();
success_ = false;
error_ = "Unable to set socket descriptor"_L1;
Q_EMIT Finished();
return;
@@ -118,10 +115,6 @@ void LocalRedirectServer::ReadyRead() {
socket_->deleteLater();
socket_ = nullptr;
request_url_ = ParseUrlFromRequest(buffer_);
success_ = request_url_.isValid();
if (!request_url_.isValid()) {
error_ = "Invalid request URL"_L1;
}
close();
Q_EMIT Finished();
}
@@ -176,9 +169,9 @@ QUrl LocalRedirectServer::ParseUrlFromRequest(const QByteArray &request) const {
const QByteArrayList lines = request.split('\r');
const QByteArray &request_line = lines[0];
const QByteArray path = request_line.split(' ')[1];
const QUrl base_url = url_;
const QUrl request_url(base_url.toString() + QString::fromLatin1(path.mid(1)), QUrl::StrictMode);
QByteArray path = request_line.split(' ')[1];
QUrl base_url = url_;
QUrl request_url(base_url.toString() + QString::fromLatin1(path.mid(1)), QUrl::StrictMode);
return request_url;

View File

@@ -1,7 +1,8 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2018-2021, 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
@@ -38,16 +39,12 @@ class LocalRedirectServer : public QTcpServer {
explicit LocalRedirectServer(QObject *parent = nullptr);
~LocalRedirectServer() override;
void set_port(const int port) { port_ = port; }
bool Listen();
const QUrl &url() const { return url_; }
const QUrl &request_url() const { return request_url_; }
bool success() const { return success_; }
const QString &error() const { return error_; }
int port() const { return port_; }
void set_port(const int port) { port_ = port; }
bool Listen();
Q_SIGNALS:
void Finished();
@@ -69,7 +66,6 @@ class LocalRedirectServer : public QTcpServer {
QUrl request_url_;
QAbstractSocket *socket_;
QByteArray buffer_;
bool success_;
QString error_;
};

View File

@@ -88,4 +88,6 @@ int MultiSortFilterProxy::Compare(const QVariant &left, const QVariant &right) c
}
}
return 0;
}

View File

@@ -86,7 +86,7 @@ void NetworkProxyFactory::ReloadSettings() {
mode_ = static_cast<Mode>(s.value("mode", static_cast<int>(Mode::System)).toInt());
type_ = QNetworkProxy::ProxyType(s.value("type", QNetworkProxy::HttpProxy).toInt());
hostname_ = s.value("hostname").toString();
port_ = s.value("port", 8080).toULongLong();
port_ = s.value("port", 8080).toInt();
use_authentication_ = s.value("use_authentication", false).toBool();
username_ = s.value("username").toString();
password_ = s.value("password").toString();

View File

@@ -57,7 +57,7 @@ class NetworkProxyFactory : public QNetworkProxyFactory {
Mode mode_;
QNetworkProxy::ProxyType type_;
QString hostname_;
quint64 port_;
int port_;
bool use_authentication_;
QString username_;
QString password_;

View File

@@ -1,607 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2022-2025, 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 "config.h"
#include <algorithm>
#include <QObject>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QDateTime>
#include <QTimer>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QCryptographicHash>
#include <QJsonDocument>
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonParseError>
#include <QDesktopServices>
#include <QMessageBox>
#include "constants/timeconstants.h"
#include "utilities/randutils.h"
#include "logging.h"
#include "settings.h"
#include "networkaccessmanager.h"
#include "localredirectserver.h"
#include "oauthenticator.h"
using namespace Qt::Literals::StringLiterals;
using std::make_shared;
using namespace std::chrono_literals;
namespace {
constexpr char kTokenType[] = "token_type";
constexpr char kAccessToken[] = "access_token";
constexpr char kRefreshToken[] = "refresh_token";
constexpr char kExpiresIn[] = "expires_in";
constexpr char kLoginTime[] = "login_time";
constexpr char kUserId[] = "user_id";
constexpr char kCountryCode[] = "country_code";
constexpr int kMaxPortInc = 20;
} // namespace
OAuthenticator::OAuthenticator(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: QObject(parent),
network_(network),
timer_refresh_login_(new QTimer(this)),
type_(Type::Authorization_Code),
use_local_redirect_server_(true),
random_port_(true),
expires_in_(0LL),
login_time_(0LL),
user_id_(0) {
timer_refresh_login_->setSingleShot(true);
QObject::connect(timer_refresh_login_, &QTimer::timeout, this, &OAuthenticator::RerefreshAccessToken);
}
OAuthenticator::~OAuthenticator() {
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
QObject::disconnect(reply, nullptr, this, nullptr);
reply->abort();
reply->deleteLater();
}
}
void OAuthenticator::set_settings_group(const QString &settings_group) {
settings_group_ = settings_group;
}
void OAuthenticator::set_type(const Type type) {
type_ = type;
}
void OAuthenticator::set_authorize_url(const QUrl &authorize_url) {
authorize_url_ = authorize_url;
}
void OAuthenticator::set_redirect_url(const QUrl &redirect_url) {
redirect_url_ = redirect_url;
}
void OAuthenticator::set_access_token_url(const QUrl &access_token_url) {
access_token_url_ = access_token_url;
}
void OAuthenticator::set_client_id(const QString &client_id) {
client_id_ = client_id;
}
void OAuthenticator::set_client_secret(const QString &client_secret) {
client_secret_ = client_secret;
}
void OAuthenticator::set_scope(const QString &scope) {
scope_ = scope;
}
void OAuthenticator::set_use_local_redirect_server(const bool use_local_redirect_server) {
use_local_redirect_server_ = use_local_redirect_server;
}
void OAuthenticator::set_random_port(const bool random_port) {
random_port_ = random_port;
}
QByteArray OAuthenticator::authorization_header() const {
if (token_type_.isEmpty() || access_token_.isEmpty()) {
return QByteArray();
}
return token_type().toUtf8() + " " + access_token().toUtf8();
}
QString OAuthenticator::GrantType() const {
switch (type_) {
case Type::Authorization_Code:
return u"authorization_code"_s;
break;
case Type::Client_Credentials:
return u"client_credentials"_s;
break;
}
return QString();
}
void OAuthenticator::LoadSession() {
Settings s;
s.beginGroup(settings_group_);
token_type_ = s.value(kTokenType).toString();
access_token_ = s.value(kAccessToken).toString();
refresh_token_ = s.value(kRefreshToken).toString();
expires_in_ = s.value(kExpiresIn, 0LL).toLongLong();
login_time_ = s.value(kLoginTime, 0LL).toLongLong();
country_code_ = s.value(kCountryCode).toString();
user_id_ = s.value(kUserId).toULongLong();
s.endGroup();
StartRefreshLoginTimer();
}
void OAuthenticator::ClearSession() {
token_type_.clear();
access_token_.clear();
refresh_token_.clear();
expires_in_ = 0;
login_time_ = 0;
country_code_.clear();
user_id_ = 0;
Settings s;
s.beginGroup(settings_group_);
s.remove(kTokenType);
s.remove(kAccessToken);
s.remove(kRefreshToken);
s.remove(kExpiresIn);
s.remove(kLoginTime);
s.remove(kCountryCode);
s.remove(kUserId);
s.endGroup();
if (timer_refresh_login_->isActive()) {
timer_refresh_login_->stop();
}
}
void OAuthenticator::StartRefreshLoginTimer() {
if (login_time_ > 0 && !refresh_token_.isEmpty() && expires_in_ > 0) {
const qint64 time = std::max(1LL, expires_in_ - (QDateTime::currentSecsSinceEpoch() - login_time_));
qLog(Debug) << settings_group_ << "Refreshing login in" << time << "seconds";
timer_refresh_login_->setInterval(static_cast<int>(time * kMsecPerSec));
if (!timer_refresh_login_->isActive()) {
timer_refresh_login_->start();
}
}
}
void OAuthenticator::Authenticate() {
if (type_ == Type::Client_Credentials) {
RequestAccessToken();
return;
}
QUrl redirect_url(redirect_url_);
if (use_local_redirect_server_) {
local_redirect_server_.reset(new LocalRedirectServer(this));
bool success = false;
if (random_port_) {
success = local_redirect_server_->Listen();
}
else {
const int max_port = redirect_url.port() + kMaxPortInc;
for (int port = redirect_url.port(); port < max_port; ++port) {
local_redirect_server_->set_port(port);
if (local_redirect_server_->Listen()) {
success = true;
break;
}
}
}
if (!success) {
Q_EMIT AuthenticationFinished(false, local_redirect_server_->error());
local_redirect_server_.reset();
return;
}
QObject::connect(&*local_redirect_server_, &LocalRedirectServer::Finished, this, &OAuthenticator::RedirectArrived);
redirect_url.setPort(local_redirect_server_->port());
}
code_verifier_ = Utilities::CryptographicRandomString(44);
code_challenge_ = QString::fromLatin1(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding));
if (code_challenge_.lastIndexOf(u'=') == code_challenge_.length() - 1) {
code_challenge_.chop(1);
}
ParamList params = ParamList() << Param(u"response_type"_s, u"code"_s)
<< Param(u"redirect_uri"_s, redirect_url.toString())
<< Param(u"state"_s, code_challenge_)
<< Param(u"code_challenge_method"_s, u"S256"_s)
<< Param(u"code_challenge"_s, code_challenge_);
if (!client_id_.isEmpty()) {
params << Param(u"client_id"_s, client_id_);
}
if (!scope_.isEmpty()) {
params << Param(u"scope"_s, scope_);
}
std::sort(params.begin(), params.end());
QUrlQuery url_query;
for (const Param &param : std::as_const(params)) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first, ";")), QString::fromLatin1(QUrl::toPercentEncoding(param.second, ";")));
}
QUrl url(authorize_url_);
url.setQuery(url_query);
const bool success = QDesktopServices::openUrl(url);
if (!success) {
QMessageBox messagebox(QMessageBox::Information, tr("Authentication"), tr("Please open this URL in your browser") + QStringLiteral(":<br /><a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
messagebox.setTextFormat(Qt::RichText);
messagebox.exec();
}
}
void OAuthenticator::RedirectArrived() {
if (local_redirect_server_.isNull()) {
return;
}
if (local_redirect_server_->success()) {
QUrl redirect_url(redirect_url_);
redirect_url.setPort(local_redirect_server_->port());
AuthorizationUrlReceived(local_redirect_server_->request_url(), redirect_url);
}
else {
Q_EMIT AuthenticationFinished(false, local_redirect_server_->error());
}
local_redirect_server_.reset();
}
void OAuthenticator::ExternalAuthorizationUrlReceived(const QUrl &request_url) {
AuthorizationUrlReceived(request_url, redirect_url_);
}
void OAuthenticator::AuthorizationUrlReceived(const QUrl &request_url, const QUrl &redirect_url) {
if (!request_url.isValid()) {
Q_EMIT AuthenticationFinished(false, tr("Received invalid reply from web browser."));
return;
}
if (!request_url.hasQuery()) {
Q_EMIT AuthenticationFinished(false, tr("Redirect URL is missing query."));
return;
}
qLog(Debug) << settings_group_ << "Authorization URL Received" << request_url.toDisplayString();
QUrlQuery url_query(request_url);
if (url_query.hasQueryItem(u"error_description"_s)) {
Q_EMIT AuthenticationFinished(false, url_query.queryItemValue(u"error_description"_s, QUrl::FullyDecoded));
return;
}
if (url_query.hasQueryItem(u"error"_s)) {
Q_EMIT AuthenticationFinished(false, url_query.queryItemValue(u"error"_s));
return;
}
if (!url_query.hasQueryItem(u"code"_s)) {
Q_EMIT AuthenticationFinished(false, tr("Request URL is missing code!"));
return;
}
if (!url_query.hasQueryItem(u"state"_s)) {
Q_EMIT AuthenticationFinished(false, tr("Request URL is missing state!"));
return;
}
if (url_query.queryItemValue(u"state"_s) != code_challenge_) {
Q_EMIT AuthenticationFinished(false, tr("Request URL has wrong state %1 != %2").arg(url_query.queryItemValue(u"state"_s), code_challenge_));
return;
}
RequestAccessToken(url_query.queryItemValue(u"code"_s), redirect_url);
}
QNetworkReply *OAuthenticator::CreateAccessTokenRequest(const ParamList &params, const bool refresh_token) {
QUrlQuery url_query;
for (const Param &param : std::as_const(params)) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
QNetworkRequest network_request(access_token_url_);
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
if (type_ == Type::Client_Credentials && !client_id_.isEmpty() && !client_secret_.isEmpty()) {
const QString authorization_header = client_id_ + u':' + client_secret_;
network_request.setRawHeader("Authorization", "Basic " + authorization_header.toUtf8().toBase64());
}
QNetworkReply *reply = network_->post(network_request, url_query.toString(QUrl::FullyEncoded).toUtf8());
replies_ << reply;
QObject::connect(reply, &QNetworkReply::sslErrors, this, &OAuthenticator::HandleSSLErrors);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, refresh_token]() { AccessTokenRequestFinished(reply, refresh_token); });
return reply;
}
void OAuthenticator::RequestAccessToken(const QString &code, const QUrl &redirect_url) {
if (timer_refresh_login_->isActive()) {
timer_refresh_login_->stop();
}
ParamList params = ParamList() << Param(u"grant_type"_s, GrantType());
if (!code.isEmpty()) {
params << Param(u"code"_s, code);
}
if (!code_verifier_.isEmpty()) {
params << Param(u"code_verifier"_s, code_verifier_);
}
if (!code.isEmpty()) {
params << Param(u"redirect_uri"_s, redirect_url.toString());
}
if (!client_id_.isEmpty()) {
params << Param(u"client_id"_s, client_id_);
}
if (!client_secret_.isEmpty()) {
params << Param(u"client_secret"_s, client_secret_);
}
std::sort(params.begin(), params.end());
CreateAccessTokenRequest(params, false);
}
void OAuthenticator::RerefreshAccessToken() {
if (timer_refresh_login_->isActive()) {
timer_refresh_login_->stop();
}
if (client_id_.isEmpty() || refresh_token_.isEmpty()) {
return;
}
ParamList params = ParamList() << Param(u"grant_type"_s, u"refresh_token"_s)
<< Param(u"client_id"_s, client_id_)
<< Param(u"refresh_token"_s, refresh_token_);
if (!client_secret_.isEmpty()) {
params << Param(u"client_secret"_s, client_secret_);
}
CreateAccessTokenRequest(params, true);
}
void OAuthenticator::HandleSSLErrors(const QList<QSslError> &ssl_errors) {
for (const QSslError &ssl_error : ssl_errors) {
qLog(Debug) << settings_group_ << ssl_error.errorString();
}
}
void OAuthenticator::AccessTokenRequestFinished(QNetworkReply *reply, const bool refresh_token) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
const QString error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Q_EMIT AuthenticationFinished(false, error_message);
}
if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
const QByteArray data = reply->readAll();
if (!data.isEmpty()) {
QJsonParseError json_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
if (json_error.error == QJsonParseError::NoError && !json_document.isEmpty() && json_document.isObject()) {
const QJsonObject json_object = json_document.object();
if (json_object.contains("error"_L1) && json_object.contains("error_description"_L1)) {
const QString error = json_object["error"_L1].toString();
const QString error_description = json_object["error_description"_L1].toString();
Q_EMIT AuthenticationFinished(false, QStringLiteral("%1 (%2)").arg(error, error_description));
return;
}
qLog(Debug) << settings_group_ << "Unknown Json reply" << json_object;
}
}
if (reply->error() == QNetworkReply::NoError) {
Q_EMIT AuthenticationFinished(false, QStringLiteral("Received HTTP status code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
}
else {
Q_EMIT AuthenticationFinished(false, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
return;
}
const QByteArray data = reply->readAll();
QJsonParseError json_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
if (json_error.error != QJsonParseError::NoError) {
Q_EMIT AuthenticationFinished(false, QStringLiteral("Failed to parse Json data in authentication reply: %1").arg(json_error.errorString()));
return;
}
if (json_document.isEmpty()) {
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server has empty Json document."_s);
return;
}
if (!json_document.isObject()) {
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server has Json document that is not an object."_s);
return;
}
const QJsonObject json_object = json_document.object();
if (json_object.isEmpty()) {
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server has empty Json object."_s);
return;
}
if (!json_object.contains("token_type"_L1)) {
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server is missing token type."_s);
return;
}
if (!json_object.contains("access_token"_L1)) {
Q_EMIT AuthenticationFinished(false, u"Authentication reply from server is missing access token."_s);
return;
}
token_type_ = json_object["token_type"_L1].toString();
access_token_ = json_object["access_token"_L1].toString();
if (json_object.contains("refresh_token"_L1)) {
refresh_token_ = json_object["refresh_token"_L1].toString();
}
else if (!refresh_token) {
refresh_token_.clear();
}
if (json_object.contains("expires_in"_L1)) {
expires_in_ = json_object["expires_in"_L1].toInt();
}
else {
expires_in_ = 0;
}
login_time_ = QDateTime::currentSecsSinceEpoch();
country_code_.clear();
user_id_ = 0;
if (json_object.contains("user"_L1) && json_object["user"_L1].isObject()) {
const QJsonObject object_user = json_object["user"_L1].toObject();
if (object_user.contains("countryCode"_L1) && object_user.contains("userId"_L1)) {
country_code_ = object_user["countryCode"_L1].toString();
user_id_ = static_cast<quint64>(object_user["userId"_L1].toInt());
}
}
Settings s;
s.beginGroup(settings_group_);
s.setValue(kTokenType, token_type_);
s.setValue(kAccessToken, access_token_);
s.setValue(kLoginTime, login_time_);
if (refresh_token_.isEmpty()) {
s.remove(kRefreshToken);
}
else {
s.setValue(kRefreshToken, refresh_token_);
}
if (expires_in_ == 0) {
s.remove(kExpiresIn);
}
else {
s.setValue(kExpiresIn, expires_in_);
}
if (country_code_.isEmpty()) {
s.remove(kCountryCode);
}
else {
s.setValue(kCountryCode, country_code_);
}
if (user_id_ == 0) {
s.remove(kUserId);
}
else {
s.setValue(kUserId, user_id_);
}
s.endGroup();
StartRefreshLoginTimer();
qLog(Debug) << settings_group_ << "Authentication was successful, login expires in" << expires_in_;
Q_EMIT AuthenticationFinished(true);
}

View File

@@ -1,127 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2022-2025, 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 OAUTHENTICATOR_H
#define OAUTHENTICATOR_H
#include "config.h"
#include <QObject>
#include <QList>
#include <QString>
#include <QUrl>
#include <QScopedPointer>
#include <QSharedPointer>
#include <QSslError>
#include "includes/shared_ptr.h"
class QTimer;
class QNetworkReply;
class NetworkAccessManager;
class LocalRedirectServer;
class OAuthenticator : public QObject {
Q_OBJECT
public:
explicit OAuthenticator(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
~OAuthenticator() override;
enum class Type {
Authorization_Code,
Client_Credentials
};
void set_settings_group(const QString &settings_group);
void set_type(const Type type);
void set_authorize_url(const QUrl &auth_url);
void set_redirect_url(const QUrl &redirect_url);
void set_access_token_url(const QUrl &access_token_url);
void set_client_id(const QString &client_id);
void set_client_secret(const QString &client_secret);
void set_scope(const QString &scope);
void set_use_local_redirect_server(const bool use_local_redirect_server);
void set_random_port(const bool random_port);
QString token_type() const { return token_type_; }
QString access_token() const { return access_token_; }
qint64 expires_in() const { return expires_in_; }
QString country_code() const { return country_code_; }
quint64 user_id() const { return user_id_; }
bool authenticated() const { return !token_type_.isEmpty() && !access_token_.isEmpty(); }
QByteArray authorization_header() const;
void Authenticate();
void ClearSession();
void LoadSession();
void ExternalAuthorizationUrlReceived(const QUrl &request_url);
private:
using Param = QPair<QString, QString>;
using ParamList = QList<Param>;
QString GrantType() const;
void StartRefreshLoginTimer();
QNetworkReply *CreateAccessTokenRequest(const ParamList &params, const bool refresh_token);
void RequestAccessToken(const QString &code = QString(), const QUrl &redirect_url = QUrl());
void RerefreshAccessToken();
void AuthorizationUrlReceived(const QUrl &request_url, const QUrl &redirect_url);
Q_SIGNALS:
void Error(const QString &error);
void AuthenticationFinished(const bool success, const QString &error = QString());
private Q_SLOTS:
void RedirectArrived();
void HandleSSLErrors(const QList<QSslError> &ssl_errors);
void AccessTokenRequestFinished(QNetworkReply *reply, const bool refresh_token);
private:
const SharedPtr<NetworkAccessManager> network_;
QScopedPointer<LocalRedirectServer, QScopedPointerDeleteLater> local_redirect_server_;
QTimer *timer_refresh_login_;
QString settings_group_;
Type type_;
QUrl authorize_url_;
QUrl redirect_url_;
QUrl access_token_url_;
QString client_id_;
QString client_secret_;
QString scope_;
bool use_local_redirect_server_;
bool random_port_;
QString code_verifier_;
QString code_challenge_;
QString token_type_;
QString access_token_;
QString refresh_token_;
qint64 expires_in_;
qint64 login_time_;
QString country_code_;
quint64 user_id_;
QList<QNetworkReply*> replies_;
};
#endif // OAUTHENTICATOR_H

View File

@@ -439,7 +439,6 @@ int Song::samplerate() const { return d->samplerate_; }
int Song::bitdepth() const { return d->bitdepth_; }
Song::Source Song::source() const { return d->source_; }
int Song::source_id() const { return static_cast<int>(d->source_); }
int Song::directory_id() const { return d->directory_id_; }
const QUrl &Song::url() const { return d->url_; }
const QString &Song::basefilename() const { return d->basefilename_; }
@@ -662,8 +661,7 @@ const QString &Song::playlist_albumartist() const { return is_compilation() ? d-
const QString &Song::playlist_albumartist_sortable() const { return is_compilation() ? d->albumartist_sortable_ : effective_albumartist_sortable(); }
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
bool Song::is_linked_collection_song() const { return IsLinkedCollectionSource(d->source_); }
bool Song::is_collection_song() const { return d->source_ == Source::Collection; }
bool Song::is_stream() const { return is_radio() || d->source_ == Source::Tidal || d->source_ == Source::Subsonic || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; }
bool Song::is_cdda() const { return d->source_ == Source::CDDA; }
@@ -715,8 +713,7 @@ bool Song::additional_tags_supported() const {
d->filetype_ == FileType::MP4 ||
d->filetype_ == FileType::MPC ||
d->filetype_ == FileType::APE ||
d->filetype_ == FileType::WAV ||
d->filetype_ == FileType::AIFF;
d->filetype_ == FileType::WAV;
}
@@ -739,8 +736,7 @@ bool Song::performer_supported() const {
d->filetype_ == FileType::MPEG ||
d->filetype_ == FileType::MPC ||
d->filetype_ == FileType::APE ||
d->filetype_ == FileType::WAV ||
d->filetype_ == FileType::AIFF;
d->filetype_ == FileType::WAV;
}
@@ -768,9 +764,7 @@ bool Song::rating_supported() const {
d->filetype_ == FileType::MP4 ||
d->filetype_ == FileType::ASF ||
d->filetype_ == FileType::MPC ||
d->filetype_ == FileType::APE ||
d->filetype_ == FileType::WAV ||
d->filetype_ == FileType::AIFF;
d->filetype_ == FileType::APE;
}
@@ -788,9 +782,7 @@ bool Song::save_embedded_cover_supported(const FileType filetype) {
filetype == FileType::OggVorbis ||
filetype == FileType::OggOpus ||
filetype == FileType::MPEG ||
filetype == FileType::MP4 ||
filetype == FileType::WAV ||
filetype == FileType::AIFF;
filetype == FileType::MP4;
}
@@ -1035,10 +1027,8 @@ bool Song::IsOnSameAlbum(const Song &other) const {
bool Song::IsSimilar(const Song &other) const {
return title().compare(other.title(), Qt::CaseInsensitive) == 0 &&
artist().compare(other.artist(), Qt::CaseInsensitive) == 0 &&
album().compare(other.album(), Qt::CaseInsensitive) == 0 &&
fingerprint().compare(other.fingerprint()) == 0 &&
acoustid_fingerprint().compare(other.acoustid_fingerprint()) == 0;
artist().compare(other.artist(), Qt::CaseInsensitive) == 0 &&
album().compare(other.album(), Qt::CaseInsensitive) == 0;
}
Song::Source Song::SourceFromURL(const QUrl &url) {
@@ -1341,12 +1331,6 @@ Song::FileType Song::FiletypeByExtension(const QString &ext) {
}
bool Song::IsLinkedCollectionSource(const Source source) {
return source == Source::Collection;
}
QString Song::ImageCacheDir(const Source source) {
switch (source) {
@@ -1847,8 +1831,8 @@ bool Song::MergeFromEngineMetadata(const EngineMetadata &engine_metadata) {
bool minor = true;
if (d->init_from_file_ || is_local_collection_song() || d->url_.isLocalFile()) {
// This Song was already loaded using TagLib. Our tags are probably better than the engine's.
if (d->init_from_file_ || is_collection_song() || d->url_.isLocalFile()) {
// This Song was already loaded using taglib. Our tags are probably better than the engine's.
if (title() != engine_metadata.title && title().isEmpty() && !engine_metadata.title.isEmpty()) {
set_title(engine_metadata.title);
minor = false;
@@ -1925,7 +1909,7 @@ size_t qHash(const Song &song) {
size_t HashSimilar(const Song &song) {
// Should compare the same fields as function IsSimilar
return qHash(song.title().toLower()) ^ qHash(song.artist().toLower()) ^ qHash(song.album().toLower()) ^ qHash(song.fingerprint()) ^ qHash(song.acoustid_fingerprint());
return qHash(song.title().toLower()) ^ qHash(song.artist().toLower()) ^ qHash(song.album().toLower());
}
bool Song::ContainsRegexList(const QString &str, const RegularExpressionList &regex_list) {

View File

@@ -78,7 +78,6 @@ class Song {
RadioParadise = 10,
Spotify = 11
};
static const int kSourceCount = 16;
enum class FileType {
Unknown = 0,
@@ -177,7 +176,6 @@ class Song {
int bitdepth() const;
Source source() const;
int source_id() const;
int directory_id() const;
const QUrl &url() const;
const QString &basefilename() const;
@@ -374,8 +372,7 @@ class Song {
const QString &playlist_albumartist_sortable() const;
bool is_metadata_good() const;
bool is_local_collection_song() const;
bool is_linked_collection_song() const;
bool is_collection_song() const;
bool is_stream() const;
bool is_radio() const;
bool is_cdda() const;
@@ -462,7 +459,6 @@ class Song {
static FileType FiletypeByMimetype(const QString &mimetype);
static FileType FiletypeByDescription(const QString &text);
static FileType FiletypeByExtension(const QString &ext);
static bool IsLinkedCollectionSource(const Source source);
static QString ImageCacheDir(const Source source);
// Sort songs alphabetically using their pretty title

View File

@@ -55,7 +55,7 @@ void StandardItemIconLoader::SetModel(QAbstractItemModel *model) {
}
void StandardItemIconLoader::LoadAlbumCover(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item) {
void StandardItemIconLoader::LoadIcon(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item) {
AlbumCoverLoaderOptions cover_options(AlbumCoverLoaderOptions::Option::ScaledImage);
cover_options.desired_scaled_size = QSize(16, 16);
@@ -64,7 +64,7 @@ void StandardItemIconLoader::LoadAlbumCover(const QUrl &art_automatic, const QUr
}
void StandardItemIconLoader::LoadAlbumCover(const Song &song, QStandardItem *for_item) {
void StandardItemIconLoader::LoadIcon(const Song &song, QStandardItem *for_item) {
AlbumCoverLoaderOptions cover_options(AlbumCoverLoaderOptions::Option::ScaledImage);
cover_options.desired_scaled_size = QSize(16, 16);

View File

@@ -46,8 +46,8 @@ class StandardItemIconLoader : public QObject {
void SetModel(QAbstractItemModel *model);
void LoadAlbumCover(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item);
void LoadAlbumCover(const Song &song, QStandardItem *for_item);
void LoadIcon(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item);
void LoadIcon(const Song &song, QStandardItem *for_item);
private Q_SLOTS:
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);

View File

@@ -28,29 +28,21 @@
UrlHandler::UrlHandler(QObject *parent) : QObject(parent) {}
UrlHandler::LoadResult::LoadResult(const QUrl &media_url,
const Type type,
const QUrl &stream_url,
const Song::FileType filetype,
const int samplerate,
const int bit_depth,
const qint64 length_nanosec,
const QString &error)
: media_url_(media_url),
type_(type),
stream_url_(stream_url),
filetype_(filetype),
samplerate_(samplerate),
bit_depth_(bit_depth),
length_nanosec_(length_nanosec),
error_(error) {}
UrlHandler::LoadResult::LoadResult(const QUrl &media_url, const Type type, const QString &error)
: media_url_(media_url),
type_(type),
filetype_(Song::FileType::Stream),
samplerate_(-1),
bit_depth_(-1),
length_nanosec_(-1),
error_(error) {}
UrlHandler::LoadResult::LoadResult(const QUrl &media_url, const Type type, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 length_nanosec, const QString &error) : media_url_(media_url),
type_(type),
stream_url_(stream_url),
filetype_(filetype),
samplerate_(samplerate),
bit_depth_(bit_depth),
length_nanosec_(length_nanosec),
error_(error)
{}
UrlHandler::LoadResult::LoadResult(const QUrl &media_url, const Type type, const QString &error) : media_url_(media_url),
type_(type),
filetype_(Song::FileType::Stream),
samplerate_(-1),
bit_depth_(-1),
length_nanosec_(-1),
error_(error)
{}

View File

@@ -56,6 +56,7 @@ class UrlHandler : public QObject {
};
explicit LoadResult(const QUrl &media_url = QUrl(), const Type type = Type::NoMoreTracks, const QUrl &stream_url = QUrl(), const Song::FileType filetype = Song::FileType::Stream, const int samplerate = -1, const int bit_depth = -1, const qint64 length_nanosec = -1, const QString &error = QString());
explicit LoadResult(const QUrl &media_url, const Type type, const QString &error);
// The url that the playlist item has in Url().

View File

@@ -140,12 +140,12 @@ void Windows7ThumbBar::HandleWinEvent(MSG *msg) {
for (int i = 0; i < actions_.count(); ++i) {
const QAction *action = actions_[i];
THUMBBUTTON *button = &buttons[i];
button->iId = static_cast<UINT>(i);
button->iId = i;
SetupButton(action, button);
}
qLog(Debug) << "Adding" << actions_.count() << "buttons";
HRESULT hr = taskbar_list_->ThumbBarAddButtons(reinterpret_cast<HWND>(widget_->winId()), static_cast<UINT>(actions_.count()), buttons);
HRESULT hr = taskbar_list_->ThumbBarAddButtons(reinterpret_cast<HWND>(widget_->winId()), actions_.count(), buttons);
if (hr != S_OK) {
qLog(Debug) << "Failed to add buttons" << Qt::hex << DWORD(hr);
}
@@ -185,11 +185,11 @@ void Windows7ThumbBar::ActionChanged() {
QAction *action = actions_[i];
THUMBBUTTON *button = &buttons[i];
button->iId = static_cast<UINT>(i);
button->iId = i;
SetupButton(action, button);
}
HRESULT hr = taskbar_list_->ThumbBarUpdateButtons(reinterpret_cast<HWND>(widget_->winId()), static_cast<UINT>(actions_.count()), buttons);
HRESULT hr = taskbar_list_->ThumbBarUpdateButtons(reinterpret_cast<HWND>(widget_->winId()), actions_.count(), buttons);
if (hr != S_OK) {
qLog(Debug) << "Failed to update buttons" << Qt::hex << DWORD(hr);
}

View File

@@ -0,0 +1,119 @@
set(COVERMANAGER_SOURCES
albumcovermanager.cpp
albumcovermanagerlist.cpp
albumcoverloader.cpp
albumcoverloaderoptions.cpp
albumcoverfetcher.cpp
albumcoverfetchersearch.cpp
albumcoversearcher.cpp
albumcoverexport.cpp
albumcoverexporter.cpp
albumcoverchoicecontroller.cpp
coverprovider.cpp
coverproviders.cpp
coversearchstatistics.cpp
coversearchstatisticsdialog.cpp
coverexportrunnable.cpp
currentalbumcoverloader.cpp
coverfromurldialog.cpp
jsoncoverprovider.cpp
lastfmcoverprovider.cpp
musicbrainzcoverprovider.cpp
discogscoverprovider.cpp
deezercoverprovider.cpp
musixmatchcoverprovider.cpp
opentidalcoverprovider.cpp
)
set(COVERMANAGER_HEADERS
albumcovermanager.h
albumcovermanagerlist.h
albumcoverloader.h
albumcoverfetcher.h
albumcoverfetchersearch.h
albumcoversearcher.h
albumcoverexport.h
albumcoverexporter.h
albumcoverchoicecontroller.h
coverprovider.h
coverproviders.h
coversearchstatisticsdialog.h
coverexportrunnable.h
currentalbumcoverloader.h
coverfromurldialog.h
jsoncoverprovider.h
lastfmcoverprovider.h
musicbrainzcoverprovider.h
discogscoverprovider.h
deezercoverprovider.h
musixmatchcoverprovider.h
opentidalcoverprovider.h
)
set(COVERMANAGER_UI
albumcoverexport.ui
albumcovermanager.ui
albumcoversearcher.ui
coversearchstatisticsdialog.ui
coverfromurldialog.ui
)
if(HAVE_TIDAL)
list(APPEND COVERMANAGER_SOURCES tidalcoverprovider.cpp)
list(APPEND COVERMANAGER_HEADERS tidalcoverprovider.h)
endif()
if(HAVE_SPOTIFY)
list(APPEND COVERMANAGER_SOURCES spotifycoverprovider.cpp)
list(APPEND COVERMANAGER_HEADERS spotifycoverprovider.h)
endif()
if(HAVE_QOBUZ)
list(APPEND COVERMANAGER_SOURCES qobuzcoverprovider.cpp)
list(APPEND COVERMANAGER_HEADERS qobuzcoverprovider.h)
endif()
qt_wrap_cpp(COVERMANAGER_SOURCES ${COVERMANAGER_HEADERS})
qt_wrap_ui(COVERMANAGER_SOURCES ${COVERMANAGER_UI})
add_library(strawberry_covermanager STATIC ${COVERMANAGER_SOURCES})
target_include_directories(strawberry_covermanager PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/src
)
target_link_libraries(strawberry_covermanager PRIVATE
PkgConfig::GLIB
PkgConfig::GOBJECT
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Sql
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
strawberry_utilities
strawberry_core
strawberry_tagreader
strawberry_collection
strawberry_streaming
strawberry_widgets
)
if(HAVE_SUBSONIC)
target_link_libraries(strawberry_covermanager PRIVATE strawberry_subsonic)
endif()
if(HAVE_TIDAL)
target_link_libraries(strawberry_covermanager PRIVATE strawberry_tidal)
endif()
if(HAVE_SPOTIFY)
target_link_libraries(strawberry_covermanager PRIVATE strawberry_spotify)
endif()
if(HAVE_QOBUZ)
target_link_libraries(strawberry_covermanager PRIVATE strawberry_qobuz)
endif()

View File

@@ -101,7 +101,7 @@ void AlbumCoverFetcherSearch::Start(SharedPtr<CoverProviders> cover_providers) {
if (!provider->is_enabled()) continue;
// Skip any provider that requires authentication but is not authenticated.
if (provider->authentication_required() && !provider->authenticated()) {
if (provider->AuthenticationRequired() && !provider->IsAuthenticated()) {
continue;
}
@@ -285,9 +285,9 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
qLog(Debug) << "Loading" << result.artist << result.album << result.image_url << "from" << result.provider << "with current score" << result.score();
QNetworkRequest network_request(result.image_url);
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *image_reply = network_->get(network_request);
QNetworkRequest req(result.image_url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *image_reply = network_->get(req);
QObject::connect(image_reply, &QNetworkReply::finished, this, [this, image_reply]() { ProviderCoverFetchFinished(image_reply); });
pending_image_loads_[image_reply] = result;
image_load_timeout_->AddReply(image_reply);

View File

@@ -397,9 +397,9 @@ AlbumCoverLoader::LoadImageResult AlbumCoverLoader::LoadRemoteUrlImage(TaskPtr t
qLog(Debug) << "Loading remote cover from URL" << cover_url;
QNetworkRequest network_request(cover_url);
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = network_->get(network_request);
QNetworkRequest request(cover_url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = network_->get(request);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, task, result_type, cover_url]() { LoadRemoteImageFinished(reply, task, result_type, cover_url); });
return LoadImageResult(result_type, LoadImageResult::Status::Async);
@@ -418,10 +418,10 @@ void AlbumCoverLoader::LoadRemoteImageFinished(QNetworkReply *reply, TaskPtr tas
}
const QUrl redirect_url = redirect.toUrl();
qLog(Debug) << "Loading remote cover from redirected URL" << redirect_url;
QNetworkRequest network_request = reply->request();
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
network_request.setUrl(redirect_url);
QNetworkReply *redirected_reply = network_->get(network_request);
QNetworkRequest request = reply->request();
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
request.setUrl(redirect_url);
QNetworkReply *redirected_reply = network_->get(request);
QObject::connect(redirected_reply, &QNetworkReply::finished, this, [this, reply, task, result_type, redirect_url]() { LoadRemoteImageFinished(reply, task, result_type, redirect_url); });
return;
}

View File

@@ -69,7 +69,7 @@
#include "core/settings.h"
#include "core/database.h"
#include "core/networkaccessmanager.h"
#include "core/songmimedata.h"
#include "mimedata/songmimedata.h"
#include "utilities/strutils.h"
#include "utilities/fileutils.h"
#include "utilities/imageutils.h"

View File

@@ -35,7 +35,7 @@
#include "includes/scoped_ptr.h"
#include "core/song.h"
#include "core/songmimedata.h"
#include "mimedata/songmimedata.h"
#include "collection/collectionbackend.h"
#include "albumcovermanager.h"
#include "albumcovermanagerlist.h"

View File

@@ -73,10 +73,10 @@ void CoverFromURLDialog::accept() {
ui_->busy->show();
QNetworkRequest network_request(QUrl::fromUserInput(ui_->url->text()));
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkRequest req(QUrl::fromUserInput(ui_->url->text()));
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = network_->get(network_request);
QNetworkReply *reply = network_->get(req);
QObject::connect(reply, &QNetworkReply::finished, this, &CoverFromURLDialog::LoadCoverFromURLFinished);
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2021, 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
@@ -21,9 +21,10 @@
#include "config.h"
#include <QObject>
#include <QString>
#include "includes/shared_ptr.h"
#include "coverprovider.h"
CoverProvider::CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, const SharedPtr<NetworkAccessManager> network, QObject *parent) : JsonBaseRequest(network, parent), network_(network), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required), quality_(quality), batch_(batch), allow_missing_album_(allow_missing_album) {}
CoverProvider::CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, const SharedPtr<NetworkAccessManager> network, QObject *parent) : QObject(parent), network_(network), name_(name), enabled_(enabled), order_(0), authentication_required_(authentication_required), quality_(quality), batch_(batch), allow_missing_album_(allow_missing_album) {}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2021, 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
@@ -24,22 +24,26 @@
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QVariant>
#include <QString>
#include <QStringList>
#include "includes/shared_ptr.h"
#include "core/jsonbaserequest.h"
#include "albumcoverfetcher.h"
class NetworkAccessManager;
class CoverProvider : public JsonBaseRequest {
// Each implementation of this interface downloads covers from one online service.
// There are no limitations on what this service might be - last.fm, Amazon, Google Images - you name it.
class CoverProvider : public QObject {
Q_OBJECT
public:
explicit CoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, const SharedPtr<NetworkAccessManager> network, QObject *parent);
// A name (very short description) of this provider, like "last.fm".
QString name() const { return name_; }
bool is_enabled() const { return enabled_; }
int order() const { return order_; }
@@ -50,14 +54,10 @@ class CoverProvider : public JsonBaseRequest {
void set_enabled(const bool enabled) { enabled_ = enabled; }
void set_order(const int order) { order_ = order; }
virtual QString service_name() const override { return name_; }
virtual bool authentication_required() const override { return authentication_required_; }
virtual bool authenticated() const override { return true; }
virtual bool use_authorization_header() const override { return false; }
virtual QByteArray authorization_header() const override { return QByteArray(); }
bool AuthenticationRequired() const { return authentication_required_; }
virtual bool IsAuthenticated() const { return true; }
virtual void Authenticate() {}
virtual void ClearSession() {}
virtual void Deauthenticate() {}
// Starts searching for covers matching the given query text.
// Returns true if the query has been started, or false if an error occurred.
@@ -65,10 +65,12 @@ class CoverProvider : public JsonBaseRequest {
virtual bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) = 0;
virtual void CancelSearch(const int id) { Q_UNUSED(id); }
virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0;
Q_SIGNALS:
void AuthenticationFinished(const bool success, const QString &error = QString());
void AuthenticationComplete(const bool success, const QStringList &errors = QStringList());
void AuthenticationSuccess();
void AuthenticationFailure(const QString &error);
void AuthenticationFailure(const QStringList &errors);
void SearchResults(const int id, const CoverProviderSearchResults &results);
void SearchFinished(const int id, const CoverProviderSearchResults &results);

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2021, 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
@@ -22,6 +22,8 @@
#include <algorithm>
#include <utility>
#include <QtGlobal>
#include <QObject>
#include <QPair>
#include <QList>
#include <QMap>
@@ -36,7 +38,6 @@
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonArray>
#include <QScopeGuard>
#include "core/networkaccessmanager.h"
#include "core/logging.h"
@@ -51,11 +52,22 @@ using namespace Qt::Literals::StringLiterals;
namespace {
constexpr char kApiUrl[] = "https://api.deezer.com";
constexpr int kLimit = 10;
} // namespace
}
DeezerCoverProvider::DeezerCoverProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: JsonCoverProvider(u"Deezer"_s, true, false, 2.0, true, true, network, parent) {}
DeezerCoverProvider::~DeezerCoverProvider() {
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
QObject::disconnect(reply, nullptr, this, nullptr);
reply->abort();
reply->deleteLater();
}
}
bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
if (artist.isEmpty() && album.isEmpty() && title.isEmpty()) return false;
@@ -79,7 +91,17 @@ bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &albu
<< Param(u"q"_s, query)
<< Param(u"limit"_s, QString::number(kLimit));
QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kApiUrl) + QLatin1Char('/') + resource), params);
QUrlQuery url_query;
for (const Param &param : params) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
QUrl url(QLatin1String(kApiUrl) + QLatin1Char('/') + resource);
url.setQuery(url_query);
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = network_->get(req);
replies_ << reply;
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); });
return true;
@@ -88,56 +110,82 @@ bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &albu
void DeezerCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); }
JsonBaseRequest::JsonObjectResult DeezerCoverProvider::ParseJsonObject(QNetworkReply *reply) {
QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
JsonObjectResult result(ErrorCode::Success);
result.network_error = reply->error();
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
const QByteArray data = reply->readAll();
if (!data.isEmpty()) {
QJsonParseError json_parse_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error);
if (json_parse_error.error == QJsonParseError::NoError) {
const QJsonObject json_object = json_document.object();
if (json_object.contains("error"_L1) && json_object["error"_L1].isObject()) {
const QJsonObject object_error = json_object["error"_L1].toObject();
if (object_error.contains("code"_L1) && object_error.contains("type"_L1) && object_error.contains("message"_L1)) {
const int code = object_error["code"_L1].toInt();
const QString type = object_error["type"_L1].toString();
const QString message = object_error["message"_L1].toString();
result.error_code = ErrorCode::APIError;
result.error_message = QStringLiteral("%1: %2 (%3)").arg(type, message).arg(code);
}
}
else {
result.json_object = json_document.object();
}
else {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
// This is a network error, there is nothing more to do.
QString error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Error(error);
}
else {
result.error_code = ErrorCode::ParseError;
result.error_message = json_parse_error.errorString();
// See if there is Json data containing "error" object - then use that instead.
data = reply->readAll();
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
QString error;
if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("error"_L1)) {
QJsonValue value_error = json_obj["error"_L1];
if (value_error.isObject()) {
QJsonObject obj_error = value_error.toObject();
int code = obj_error["code"_L1].toInt();
QString message = obj_error["message"_L1].toString();
error = QStringLiteral("%1 (%2)").arg(message).arg(code);
}
}
}
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
Error(error);
}
return QByteArray();
}
if (result.error_code != ErrorCode::APIError) {
if (reply->error() != QNetworkReply::NoError) {
result.error_code = ErrorCode::NetworkError;
result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else if (result.http_status_code != 200) {
result.error_code = ErrorCode::HttpError;
result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code);
return data;
}
QJsonValue DeezerCoverProvider::ExtractData(const QByteArray &data) {
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) return QJsonObject();
if (json_obj.contains("error"_L1)) {
QJsonValue value_error = json_obj["error"_L1];
if (!value_error.isObject()) {
Error(u"Error missing object"_s, json_obj);
return QJsonValue();
}
QJsonObject obj_error = value_error.toObject();
const int code = obj_error["code"_L1].toInt();
QString message = obj_error["message"_L1].toString();
Error(QStringLiteral("%1 (%2)").arg(message).arg(code));
return QJsonValue();
}
return result;
if (!json_obj.contains("data"_L1) && !json_obj.contains("DATA"_L1)) {
Error(u"Json reply object is missing data."_s, json_obj);
return QJsonValue();
}
QJsonValue value_data;
if (json_obj.contains("data"_L1)) value_data = json_obj["data"_L1];
else value_data = json_obj["DATA"_L1];
return value_data;
}
@@ -148,37 +196,25 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
QObject::disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
CoverProviderSearchResults results;
const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); });
const JsonObjectResult json_object_result = ParseJsonObject(reply);
if (!json_object_result.success()) {
Error(json_object_result.error_message);
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
Q_EMIT SearchFinished(id, CoverProviderSearchResults());
return;
}
const QJsonObject &json_object = json_object_result.json_object;
if (!json_object.isEmpty()) {
return;
}
QJsonArray array_data;
if (json_object.contains("data"_L1) && json_object["DATA"_L1].isArray()) {
array_data = json_object["data"_L1].toArray();
}
else if (json_object.contains("DATA"_L1) && json_object["DATA"_L1].isArray()) {
array_data = json_object["data"_L1].toArray();
}
else {
Error(u"Json reply object is missing data."_s, json_object);
QJsonValue value_data = ExtractData(data);
if (!value_data.isArray()) {
Q_EMIT SearchFinished(id, CoverProviderSearchResults());
return;
}
QJsonArray array_data = value_data.toArray();
if (array_data.isEmpty()) {
Q_EMIT SearchFinished(id, CoverProviderSearchResults());
return;
}
QMap<QUrl, CoverProviderSearchResult> cover_results;
QMap<QUrl, CoverProviderSearchResult> results;
int i = 0;
for (const QJsonValue &json_value : std::as_const(array_data)) {
@@ -186,52 +222,52 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
Error(u"Invalid Json reply, data array value is not a object."_s);
continue;
}
const QJsonObject value_object = json_value.toObject();
QJsonObject object_album;
if (value_object.contains("album"_L1) && value_object["album"_L1].isObject()) { // Song search, so extract the album.
object_album = value_object["album"_L1].toObject();
QJsonObject json_obj = json_value.toObject();
QJsonObject obj_album;
if (json_obj.contains("album"_L1) && json_obj["album"_L1].isObject()) { // Song search, so extract the album.
obj_album = json_obj["album"_L1].toObject();
}
else {
object_album = value_object;
obj_album = json_obj;
}
if (!value_object.contains("id"_L1) || !object_album.contains("id"_L1)) {
Error(u"Invalid Json reply, data array value object is missing ID."_s, value_object);
if (!json_obj.contains("id"_L1) || !obj_album.contains("id"_L1)) {
Error(u"Invalid Json reply, data array value object is missing ID."_s, json_obj);
continue;
}
if (!object_album.contains("type"_L1)) {
Error(u"Invalid Json reply, data array value album object is missing type."_s, object_album);
if (!obj_album.contains("type"_L1)) {
Error(u"Invalid Json reply, data array value album object is missing type."_s, obj_album);
continue;
}
const QString type = object_album["type"_L1].toString();
QString type = obj_album["type"_L1].toString();
if (type != "album"_L1) {
Error(u"Invalid Json reply, data array value album object has incorrect type returned"_s, object_album);
Error(u"Invalid Json reply, data array value album object has incorrect type returned"_s, obj_album);
continue;
}
if (!json_object.contains("artist"_L1)) {
Error(u"Invalid Json reply, data array value object is missing artist."_s, json_object);
if (!json_obj.contains("artist"_L1)) {
Error(u"Invalid Json reply, data array value object is missing artist."_s, json_obj);
continue;
}
const QJsonValue value_artist = json_object["artist"_L1];
QJsonValue value_artist = json_obj["artist"_L1];
if (!value_artist.isObject()) {
Error(u"Invalid Json reply, data array value artist is not a object."_s, value_artist);
continue;
}
const QJsonObject object_artist = value_artist.toObject();
QJsonObject obj_artist = value_artist.toObject();
if (!object_artist.contains("name"_L1)) {
Error(u"Invalid Json reply, data array value artist object is missing name."_s, object_artist);
if (!obj_artist.contains("name"_L1)) {
Error(u"Invalid Json reply, data array value artist object is missing name."_s, obj_artist);
continue;
}
const QString artist = object_artist["name"_L1].toString();
QString artist = obj_artist["name"_L1].toString();
if (!object_album.contains("title"_L1)) {
Error(u"Invalid Json reply, data array value album object is missing title."_s, object_album);
if (!obj_album.contains("title"_L1)) {
Error(u"Invalid Json reply, data array value album object is missing title."_s, obj_album);
continue;
}
const QString album = object_album["title"_L1].toString();
QString album = obj_album["title"_L1].toString();
CoverProviderSearchResult cover_result;
cover_result.artist = artist;
@@ -241,29 +277,35 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
const QList<QPair<QString, QSize>> cover_sizes = QList<QPair<QString, QSize>>() << qMakePair(u"cover_xl"_s, QSize(1000, 1000))
<< qMakePair(u"cover_big"_s, QSize(500, 500));
for (const QPair<QString, QSize> &cover_size : cover_sizes) {
if (!object_album.contains(cover_size.first)) continue;
QString cover = object_album[cover_size.first].toString();
if (!obj_album.contains(cover_size.first)) continue;
QString cover = obj_album[cover_size.first].toString();
if (!have_cover) {
have_cover = true;
++i;
}
QUrl url(cover);
if (!cover_results.contains(url)) {
if (!results.contains(url)) {
cover_result.image_url = url;
cover_result.image_size = cover_size.second;
cover_result.number = i;
cover_results.insert(url, cover_result);
results.insert(url, cover_result);
}
}
if (!have_cover) {
Error(u"Invalid Json reply, data array value album object is missing cover."_s, object_album);
Error(u"Invalid Json reply, data array value album object is missing cover."_s, obj_album);
}
}
results = cover_results.values();
std::stable_sort(results.begin(), results.end(), AlbumCoverFetcherSearch::CoverProviderSearchResultCompareNumber);
if (results.isEmpty()) {
Q_EMIT SearchFinished(id, CoverProviderSearchResults());
}
else {
CoverProviderSearchResults cover_results = results.values();
std::stable_sort(cover_results.begin(), cover_results.end(), AlbumCoverFetcherSearch::CoverProviderSearchResultCompareNumber);
Q_EMIT SearchFinished(id, cover_results);
}
}

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2021, 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
@@ -22,8 +22,13 @@
#include "config.h"
#include <QObject>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QJsonValue>
#include <QJsonObject>
#include "jsoncoverprovider.h"
@@ -35,6 +40,7 @@ class DeezerCoverProvider : public JsonCoverProvider {
public:
explicit DeezerCoverProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
~DeezerCoverProvider() override;
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override;
void CancelSearch(const int id) override;
@@ -42,11 +48,13 @@ class DeezerCoverProvider : public JsonCoverProvider {
private Q_SLOTS:
void HandleSearchReply(QNetworkReply *reply, const int id);
protected:
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
private:
QByteArray GetReplyData(QNetworkReply *reply);
QJsonValue ExtractData(const QByteArray &data);
void Error(const QString &error, const QVariant &debug = QVariant()) override;
private:
void Error(const QString &error, const QVariant &debug = QVariant()) override;
QList<QNetworkReply*> replies_;
};
#endif // DEEZERCOVERPROVIDER_H

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Martin Björklund <mbj4668@gmail.com>
* Copyright 2016-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2016-2021, 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
@@ -24,6 +24,8 @@
#include <memory>
#include <algorithm>
#include <QtGlobal>
#include <QObject>
#include <QByteArray>
#include <QPair>
#include <QVariant>
@@ -38,7 +40,6 @@
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonArray>
#include <QScopeGuard>
#include "includes/shared_ptr.h"
#include "core/logging.h"
@@ -145,88 +146,81 @@ void DiscogsCoverProvider::SendSearchRequest(SharedPtr<DiscogsCoverSearchContext
}
QNetworkReply *DiscogsCoverProvider::CreateRequest(const QUrl &url, const ParamList &params) {
QNetworkReply *DiscogsCoverProvider::CreateRequest(QUrl url, const ParamList &params_provided) {
const ParamList request_params = ParamList() << Param(u"key"_s, QString::fromLatin1(QByteArray::fromBase64(kAccessKeyB64)))
<< Param(u"secret"_s, QString::fromLatin1(QByteArray::fromBase64(kSecretKeyB64)))
<< params;
const ParamList params = ParamList() << Param(u"key"_s, QString::fromLatin1(QByteArray::fromBase64(kAccessKeyB64)))
<< Param(u"secret"_s, QString::fromLatin1(QByteArray::fromBase64(kSecretKeyB64)))
<< params_provided;
QUrlQuery url_query;
QStringList query_items;
// Encode the arguments
using EncodedParam = QPair<QByteArray, QByteArray>;
for (const Param &param : request_params) {
const EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
for (const Param &param : params) {
EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
query_items << QString::fromLatin1(encoded_param.first) + QLatin1Char('=') + QString::fromLatin1(encoded_param.second);
url_query.addQueryItem(QString::fromLatin1(encoded_param.first), QString::fromLatin1(encoded_param.second));
}
QUrl request_url(url);
request_url.setQuery(url_query);
url.setQuery(url_query);
// Sign the request
const QByteArray data_to_sign = QStringLiteral("GET\n%1\n%2\n%3").arg(request_url.host(), request_url.path(), query_items.join(u'&')).toUtf8();
const QByteArray data_to_sign = QStringLiteral("GET\n%1\n%2\n%3").arg(url.host(), url.path(), query_items.join(u'&')).toUtf8();
const QByteArray signature(Utilities::HmacSha256(QByteArray::fromBase64(kSecretKeyB64), data_to_sign));
// Add the signature to the request
url_query.addQueryItem(u"Signature"_s, QString::fromLatin1(QUrl::toPercentEncoding(QString::fromLatin1(signature.toBase64()))));
QNetworkRequest network_request(request_url);
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = network_->get(network_request);
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = network_->get(req);
replies_ << reply;
qLog(Debug) << "Discogs: Sending request" << request_url;
qLog(Debug) << "Discogs: Sending request" << url;
return reply;
}
JsonBaseRequest::JsonObjectResult DiscogsCoverProvider::ParseJsonObject(QNetworkReply *reply) {
QByteArray DiscogsCoverProvider::GetReplyData(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
JsonObjectResult result(ErrorCode::Success);
result.network_error = reply->error();
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
const QByteArray data = reply->readAll();
if (!data.isEmpty()) {
QJsonParseError json_parse_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error);
if (json_parse_error.error == QJsonParseError::NoError) {
const QJsonObject json_object = json_document.object();
if (json_object.contains("message"_L1)) {
result.error_code = ErrorCode::APIError;
result.error_message = json_object["message"_L1].toString();
}
else {
result.json_object = json_document.object();
}
else {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
// This is a network error, there is nothing more to do.
QString error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
Error(error);
}
else {
result.error_code = ErrorCode::ParseError;
result.error_message = json_parse_error.errorString();
// See if there is Json data containing "message" - then use that instead.
data = reply->readAll();
QString error;
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("message"_L1)) {
error = json_obj["message"_L1].toString();
}
}
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
Error(error);
}
return QByteArray();
}
if (result.error_code != ErrorCode::APIError) {
if (reply->error() != QNetworkReply::NoError) {
result.error_code = ErrorCode::NetworkError;
result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else if (result.http_status_code != 200) {
result.error_code = ErrorCode::HttpError;
result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code);
}
}
return result;
return data;
}
@@ -240,34 +234,37 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
if (!requests_search_.contains(id)) return;
SharedPtr<DiscogsCoverSearchContext> search = requests_search_.value(id);
const QScopeGuard end_search = qScopeGuard([this, search]() { EndSearch(search); });
const JsonObjectResult json_object_result = ParseJsonObject(reply);
if (!json_object_result.success()) {
Error(json_object_result.error_message);
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
EndSearch(search);
return;
}
const QJsonObject &json_object = json_object_result.json_object;
if (json_object.isEmpty()) {
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
EndSearch(search);
return;
}
QJsonValue value_results;
if (json_object.contains("results"_L1)) {
value_results = json_object["results"_L1];
if (json_obj.contains("results"_L1)) {
value_results = json_obj["results"_L1];
}
else if (json_object.contains("message"_L1)) {
Error(json_object["message"_L1].toString());
else if (json_obj.contains("message"_L1)) {
QString message = json_obj["message"_L1].toString();
Error(QStringLiteral("%1").arg(message));
EndSearch(search);
return;
}
else {
Error(u"Json object is missing results."_s, json_object);
Error(u"Json object is missing results."_s, json_obj);
EndSearch(search);
return;
}
if (!value_results.isArray()) {
Error(u"Missing results array."_s, value_results);
EndSearch(search);
return;
}
@@ -278,19 +275,19 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
Error(u"Invalid Json reply, results value is not a object."_s);
continue;
}
const QJsonObject object_result = value_result.toObject();
if (!object_result.contains("id"_L1) || !object_result.contains("title"_L1) || !object_result.contains("resource_url"_L1)) {
Error(QStringLiteral("Invalid Json reply, results value object is missing ID, title or resource_url."), object_result);
QJsonObject obj_result = value_result.toObject();
if (!obj_result.contains("id"_L1) || !obj_result.contains("title"_L1) || !obj_result.contains("resource_url"_L1)) {
Error(QStringLiteral("Invalid Json reply, results value object is missing ID, title or resource_url."), obj_result);
continue;
}
const quint64 release_id = static_cast<quint64>(object_result["id"_L1].toInt());
const QUrl resource_url(object_result["resource_url"_L1].toString());
QString title = object_result["title"_L1].toString();
quint64 release_id = obj_result["id"_L1].toInt();
QUrl resource_url(obj_result["resource_url"_L1].toString());
QString title = obj_result["title"_L1].toString();
if (title.contains(" - "_L1)) {
QStringList title_splitted = title.split(u" - "_s);
if (title_splitted.count() == 2) {
const QString artist = title_splitted.first();
QString artist = title_splitted.first();
title = title_splitted.last();
if (artist.compare(search->artist, Qt::CaseInsensitive) != 0 && title.compare(search->album, Qt::CaseInsensitive) != 0) continue;
}
@@ -303,9 +300,14 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
StartReleaseRequest(search, release_id, resource_url);
}
if (search->requests_release_.count() == 0 && search->type == DiscogsCoverType::Master) {
search->type = DiscogsCoverType::Release;
queue_search_requests_.enqueue(search);
if (search->requests_release_.count() == 0) {
if (search->type == DiscogsCoverType::Master) {
search->type = DiscogsCoverType::Release;
queue_search_requests_.enqueue(search);
}
else {
EndSearch(search);
}
}
}
@@ -342,31 +344,33 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
if (!search->requests_release_.contains(release_id)) return;
const DiscogsCoverReleaseContext &release = search->requests_release_.value(release_id);
const QScopeGuard end_search = qScopeGuard([this, search, release]() { EndSearch(search, release.id); });
const JsonObjectResult json_object_result = ParseJsonObject(reply);
if (!json_object_result.success()) {
Error(json_object_result.error_message);
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
EndSearch(search, release.id);
return;
}
const QJsonObject &json_object = json_object_result.json_object;
if (json_object.isEmpty()) {
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
EndSearch(search, release.id);
return;
}
if (!json_object.contains("artists"_L1) || !json_object.contains("title"_L1)) {
Error(u"Json reply object is missing artists or title."_s, json_object);
if (!json_obj.contains("artists"_L1) || !json_obj.contains("title"_L1)) {
Error(u"Json reply object is missing artists or title."_s, json_obj);
EndSearch(search, release.id);
return;
}
if (!json_object.contains("images"_L1)) {
if (!json_obj.contains("images"_L1)) {
EndSearch(search, release.id);
return;
}
const QJsonValue value_artists = json_object["artists"_L1];
QJsonValue value_artists = json_obj["artists"_L1];
if (!value_artists.isArray()) {
Error(u"Json reply object artists is not a array."_s, value_artists);
EndSearch(search, release.id);
return;
}
const QJsonArray array_artists = value_artists.toArray();
@@ -377,35 +381,39 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
Error(u"Invalid Json reply, atists array value is not a object."_s);
continue;
}
const QJsonObject object_artist = value_artist.toObject();
if (!object_artist.contains("name"_L1)) {
Error(u"Invalid Json reply, artists array value object is missing name."_s, object_artist);
QJsonObject obj_artist = value_artist.toObject();
if (!obj_artist.contains("name"_L1)) {
Error(u"Invalid Json reply, artists array value object is missing name."_s, obj_artist);
continue;
}
artist = object_artist["name"_L1].toString();
artist = obj_artist["name"_L1].toString();
++i;
if (artist == search->artist) break;
}
if (artist.isEmpty()) {
EndSearch(search, release.id);
return;
}
if (i > 1 && artist != search->artist) artist = "Various artists"_L1;
const QString album = json_object["title"_L1].toString();
QString album = json_obj["title"_L1].toString();
if (artist != search->artist && album != search->album) {
EndSearch(search, release.id);
return;
}
const QJsonValue value_images = json_object["images"_L1];
QJsonValue value_images = json_obj["images"_L1];
if (!value_images.isArray()) {
Error(u"Json images is not an array."_s);
EndSearch(search, release.id);
return;
}
const QJsonArray array_images = value_images.toArray();
if (array_images.isEmpty()) {
Error(u"Invalid Json reply, images array is empty."_s);
EndSearch(search, release.id);
return;
}
@@ -415,17 +423,17 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
Error(u"Invalid Json reply, images array value is not an object."_s);
continue;
}
const QJsonObject obj_image = value_image.toObject();
QJsonObject obj_image = value_image.toObject();
if (!obj_image.contains("type"_L1) || !obj_image.contains("resource_url"_L1) || !obj_image.contains("width"_L1) || !obj_image.contains("height"_L1)) {
Error(u"Invalid Json reply, images array value object is missing type, resource_url, width or height."_s, obj_image);
continue;
}
const QString type = obj_image["type"_L1].toString();
QString type = obj_image["type"_L1].toString();
if (type != "primary"_L1) {
continue;
}
const int width = obj_image["width"_L1].toInt();
const int height = obj_image["height"_L1].toInt();
int width = obj_image["width"_L1].toInt();
int height = obj_image["height"_L1].toInt();
if (width < 300 || height < 300) continue;
const float aspect_score = static_cast<float>(1.0) - static_cast<float>(std::max(width, height) - std::min(width, height)) / static_cast<float>(std::max(height, width));
if (aspect_score < 0.85) continue;
@@ -440,6 +448,8 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
Q_EMIT SearchResults(search->id, search->results);
search->results.clear();
EndSearch(search, release.id);
}
void DiscogsCoverProvider::EndSearch(SharedPtr<DiscogsCoverSearchContext> search, const quint64 release_id) {

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Martin Björklund <mbj4668@gmail.com>
* Copyright 2016-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2016-2021, 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
@@ -24,11 +24,16 @@
#include "config.h"
#include <QObject>
#include <QMetaType>
#include <QPair>
#include <QList>
#include <QQueue>
#include <QMap>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QJsonObject>
#include "includes/shared_ptr.h"
#include "jsoncoverprovider.h"
@@ -54,7 +59,7 @@ class DiscogsCoverProvider : public JsonCoverProvider {
};
struct DiscogsCoverReleaseContext {
explicit DiscogsCoverReleaseContext(const int _search_id = 0, const quint64 _id = 0, const QUrl &_url = QUrl()) : search_id(_search_id), id(_id), url(_url) {}
explicit DiscogsCoverReleaseContext(const quint64 _search_id = 0, const quint64 _id = 0, const QUrl &_url = QUrl()) : search_id(_search_id), id(_id), url(_url) {}
int search_id;
quint64 id;
QUrl url;
@@ -72,9 +77,9 @@ class DiscogsCoverProvider : public JsonCoverProvider {
private:
void SendSearchRequest(SharedPtr<DiscogsCoverSearchContext> search);
void SendReleaseRequest(const DiscogsCoverReleaseContext &release);
QNetworkReply *CreateRequest(const QUrl &url, const ParamList &params = ParamList());
QNetworkReply *CreateRequest(QUrl url, const ParamList &params_provided = ParamList());
QByteArray GetReplyData(QNetworkReply *reply);
void StartReleaseRequest(SharedPtr<DiscogsCoverSearchContext> search, const quint64 release_id, const QUrl &url);
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
void EndSearch(SharedPtr<DiscogsCoverSearchContext> search, const quint64 release_id = 0);
void Error(const QString &error, const QVariant &debug = QVariant()) override;
@@ -88,6 +93,7 @@ class DiscogsCoverProvider : public JsonCoverProvider {
QQueue<SharedPtr<DiscogsCoverSearchContext>> queue_search_requests_;
QQueue<DiscogsCoverReleaseContext> queue_release_requests_;
QMap<int, SharedPtr<DiscogsCoverSearchContext>> requests_search_;
QList<QNetworkReply*> replies_;
};
Q_DECLARE_METATYPE(DiscogsCoverProvider::DiscogsCoverSearchContext)

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2021, 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
@@ -19,10 +19,10 @@
#include "config.h"
#include <QObject>
#include <QByteArray>
#include <QString>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QJsonObject>
#include "includes/shared_ptr.h"
@@ -35,31 +35,32 @@ using namespace Qt::Literals::StringLiterals;
JsonCoverProvider::JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, const SharedPtr<NetworkAccessManager> network, QObject *parent)
: CoverProvider(name, enabled, authentication_required, quality, batch, allow_missing_album, network, parent) {}
QJsonObject JsonCoverProvider::ExtractJsonObject(const QByteArray &data) {
QJsonObject JsonCoverProvider::ExtractJsonObj(const QByteArray &data) {
QJsonParseError json_error;
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_error);
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
if (json_error.error != QJsonParseError::NoError) {
Error(QStringLiteral("Failed to parse Json data: %1").arg(json_error.errorString()));
Error(QStringLiteral("Failed to parse json data: %1").arg(json_error.errorString()));
return QJsonObject();
}
if (json_document.isEmpty()) {
if (json_doc.isEmpty()) {
Error(u"Received empty Json document."_s, data);
return QJsonObject();
}
if (!json_document.isObject()) {
Error(u"Json document is not an object."_s, json_document);
if (!json_doc.isObject()) {
Error(u"Json document is not an object."_s, json_doc);
return QJsonObject();
}
const QJsonObject json_object = json_document.object();
if (json_object.isEmpty()) {
Error(u"Received empty Json object."_s, json_document);
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
Error(u"Received empty Json object."_s, json_doc);
return QJsonObject();
}
return json_object;
return json_obj;
}

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2021, 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
@@ -22,6 +22,7 @@
#include "config.h"
#include <QObject>
#include <QByteArray>
#include <QString>
#include <QJsonObject>
@@ -38,7 +39,7 @@ class JsonCoverProvider : public CoverProvider {
explicit JsonCoverProvider(const QString &name, const bool enabled, const bool authentication_required, const float quality, const bool batch, const bool allow_missing_album, const SharedPtr<NetworkAccessManager> network, QObject *parent);
protected:
QJsonObject ExtractJsonObject(const QByteArray &data);
QJsonObject ExtractJsonObj(const QByteArray &data);
};
#endif // JSONCOVERPROVIDER_H

Some files were not shown because too many files have changed in this diff Show More