Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b560e4e4f | ||
|
|
9e327c9556 | ||
|
|
1ec640e088 | ||
|
|
463aaf6942 | ||
|
|
71287dd77e | ||
|
|
b66c0f5573 | ||
|
|
a9f2c384fa | ||
|
|
ae9584c213 | ||
|
|
4db1c5ceb8 | ||
|
|
1738259467 | ||
|
|
fcee02edc1 | ||
|
|
4fff5820c5 | ||
|
|
9aa6da2faf | ||
|
|
279934411c | ||
|
|
fd829551e8 | ||
|
|
be8f515388 | ||
|
|
dd12462fbb | ||
|
|
5c3ded0099 | ||
|
|
2c9b14f5f2 | ||
|
|
8c195382c4 | ||
|
|
6e5198799c | ||
|
|
389d04bbec | ||
|
|
6f0f88dd01 | ||
|
|
3dce84c73c | ||
|
|
e798349aca | ||
|
|
41ecf8e535 | ||
|
|
70c96ded28 | ||
|
|
d9cc6bf238 | ||
|
|
0a70ac1c95 | ||
|
|
e3a3f44513 | ||
|
|
c2e42ebf3a | ||
|
|
d71b344e77 | ||
|
|
d5f7a4b883 | ||
|
|
34fb289e33 | ||
|
|
6d4d8251ad | ||
|
|
fc69ef27e8 | ||
|
|
bbd8a24b75 | ||
|
|
9fa9012c70 | ||
|
|
2a4fc185ac | ||
|
|
fa0703246b | ||
|
|
954c21e21e | ||
|
|
8954dfb0aa | ||
|
|
5e031be42c | ||
|
|
d5281abb22 | ||
|
|
f5368d8108 | ||
|
|
e2dc22c2c8 | ||
|
|
817ca828e6 | ||
|
|
61dc2cc640 | ||
|
|
cd4adf6f89 | ||
|
|
91ebcc948e | ||
|
|
e7e6d59172 | ||
|
|
95b12a01a4 | ||
|
|
5947aeae24 | ||
|
|
0298fa0b73 | ||
|
|
05d72c8bd6 | ||
|
|
70b7c4560d | ||
|
|
2687dc31cc | ||
|
|
cabf1cb78d | ||
|
|
d9f68ab944 | ||
|
|
8630c5329d | ||
|
|
66e175f6d1 | ||
|
|
1173d5f865 | ||
|
|
b02b114caf | ||
|
|
cd516c37b9 | ||
|
|
7de8a44709 | ||
|
|
bb345b14de | ||
|
|
baa82966d8 | ||
|
|
f85d60f5cd | ||
|
|
6f731fcf4a | ||
|
|
bdbe66b116 | ||
|
|
5ae0320911 | ||
|
|
31671af5f5 | ||
|
|
27184eb001 | ||
|
|
2e30f5c585 | ||
|
|
109d3f9ec3 | ||
|
|
e9d413c7dc | ||
|
|
ee60191b6c | ||
|
|
a6d8627129 | ||
|
|
d317c9158b | ||
|
|
3716e8c3ef | ||
|
|
ce0e1e900e | ||
|
|
f1aa92ec9f | ||
|
|
25bdfcdb76 | ||
|
|
6a3de3937a | ||
|
|
5f775e87ae | ||
|
|
1fd83c55ee | ||
|
|
e6e9edef7d | ||
|
|
1bae665f76 | ||
|
|
5bfc8b74f5 | ||
|
|
e588896729 | ||
|
|
0cd0f7f2e7 | ||
|
|
6db540a3a7 | ||
|
|
82679e0cea | ||
|
|
d571bc3305 | ||
|
|
2b52553864 | ||
|
|
215627b0e4 | ||
|
|
b17cae6ec7 | ||
|
|
30ac9697ea | ||
|
|
61e3ea249d | ||
|
|
d1986eeae2 | ||
|
|
ba354207d2 | ||
|
|
eac5674891 | ||
|
|
8349a8b0ee | ||
|
|
b9b4e9f831 | ||
|
|
98ff2525f0 | ||
|
|
3fd29c6dcc | ||
|
|
4429e9973f | ||
|
|
1572d241d5 | ||
|
|
eae7e2a9e6 | ||
|
|
251e5b379b | ||
|
|
0db082fca0 | ||
|
|
2799e55076 | ||
|
|
440ee43a91 | ||
|
|
98c72ec1f8 | ||
|
|
759488ae1a | ||
|
|
055cb413c9 | ||
|
|
f760b87b58 | ||
|
|
39f228f862 | ||
|
|
a683a279f5 | ||
|
|
e800926f57 | ||
|
|
dd9f80d539 | ||
|
|
8484cac4ed | ||
|
|
e24097582f | ||
|
|
58fc8c82bb | ||
|
|
02bb875bb3 | ||
|
|
5db01482eb | ||
|
|
719fa6ffb3 | ||
|
|
159be5d79e | ||
|
|
911237e281 | ||
|
|
ae89ca8123 |
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
125
.github/workflows/build.yml
vendored
125
.github/workflows/build.yml
vendored
@@ -68,6 +68,7 @@ jobs:
|
||||
hicolor-icon-theme
|
||||
qt6-core-devel
|
||||
qt6-gui-devel
|
||||
qt6-gui-private-devel
|
||||
qt6-widgets-devel
|
||||
qt6-concurrent-devel
|
||||
qt6-network-devel
|
||||
@@ -79,9 +80,22 @@ 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:
|
||||
@@ -192,6 +206,8 @@ jobs:
|
||||
kdsingleapplication-qt6-devel
|
||||
gtest-devel
|
||||
gmock-devel
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -228,7 +244,7 @@ jobs:
|
||||
|
||||
build-openmandriva:
|
||||
name: Build OpenMandriva
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master' && false
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -243,31 +259,30 @@ jobs:
|
||||
run: >
|
||||
dnf install -y
|
||||
which
|
||||
glibc
|
||||
gcc-c++
|
||||
git
|
||||
gnutar
|
||||
make
|
||||
cmake
|
||||
glib
|
||||
lsb-release
|
||||
rpmdevtools
|
||||
rpm-build
|
||||
glibc-devel
|
||||
boost-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
|
||||
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
|
||||
lib64Qt6Core-devel
|
||||
lib64Qt6Concurrent-devel
|
||||
lib64Qt6Network-devel
|
||||
@@ -276,6 +291,11 @@ 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
|
||||
@@ -283,6 +303,7 @@ 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
|
||||
@@ -331,12 +352,6 @@ 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
|
||||
@@ -378,10 +393,21 @@ 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:
|
||||
@@ -464,11 +490,22 @@ 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:
|
||||
@@ -501,7 +538,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'oracular' ]
|
||||
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -545,11 +582,22 @@ 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:
|
||||
@@ -583,7 +631,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'oracular' ]
|
||||
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -625,13 +673,18 @@ 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:
|
||||
@@ -644,6 +697,10 @@ 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
|
||||
@@ -676,11 +733,11 @@ jobs:
|
||||
submodules: recursive
|
||||
- name: Build FreeBSD
|
||||
id: build-freebsd
|
||||
uses: vmactions/freebsd-vm@v1.1.8
|
||||
uses: vmactions/freebsd-vm@v1.2.0
|
||||
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
|
||||
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
|
||||
run: |
|
||||
set -e
|
||||
git config --global --add safe.directory ${GITHUB_WORKSPACE}
|
||||
@@ -701,17 +758,17 @@ jobs:
|
||||
submodules: recursive
|
||||
- name: Build OpenBSD
|
||||
id: build-openbsd
|
||||
uses: vmactions/openbsd-vm@v1.1.6
|
||||
uses: vmactions/openbsd-vm@v1.1.7
|
||||
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
|
||||
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
|
||||
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
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug" -DENABLE_ALSA=OFF -DENABLE_DISCORD_RPC=OFF
|
||||
cmake --build build --config Debug --parallel 4
|
||||
|
||||
|
||||
@@ -722,7 +779,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ 'macos-13', 'macos-14' ]
|
||||
runner: [ 'macos-13', 'macos-15' ]
|
||||
buildtype: [ 'release' ]
|
||||
|
||||
runs-on: ${{ matrix.runner }}
|
||||
@@ -731,7 +788,7 @@ jobs:
|
||||
|
||||
- name: Set MACOSX_DEPLOYMENT_TARGET
|
||||
run: |
|
||||
for i in 12 13 14 15; do
|
||||
for i in 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
|
||||
@@ -768,7 +825,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@v3
|
||||
uses: apple-actions/import-codesign-certs@v5
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE }}
|
||||
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
|
||||
@@ -830,7 +887,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-14'
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
|
||||
working-directory: build
|
||||
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
||||
|
||||
@@ -889,7 +946,7 @@ jobs:
|
||||
|
||||
- name: Set MACOSX_DEPLOYMENT_TARGET
|
||||
run: |
|
||||
for i in 12 13 14 15; do
|
||||
for i in 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
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
||||
[submodule "3rdparty/kdsingleapplication/KDSingleApplication"]
|
||||
path = 3rdparty/kdsingleapplication/KDSingleApplication
|
||||
url = https://github.com/KDAB/KDSingleApplication.git
|
||||
branch = master
|
||||
|
||||
11
3rdparty/README.md
vendored
11
3rdparty/README.md
vendored
@@ -1,11 +0,0 @@
|
||||
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/
|
||||
1
3rdparty/discord-rpc/CMakeLists.txt
vendored
Normal file
1
3rdparty/discord-rpc/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
add_subdirectory(src)
|
||||
19
3rdparty/discord-rpc/LICENSE
vendored
Normal file
19
3rdparty/discord-rpc/LICENSE
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
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.
|
||||
162
3rdparty/discord-rpc/README.md
vendored
Normal file
162
3rdparty/discord-rpc/README.md
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
# 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 | [](https://travis-ci.org/discordapp/discord-rpc) |
|
||||
| AppVeyor | [](https://ci.appveyor.com/project/crmarsh/discord-rpc) |
|
||||
| Buildkite (internal) | [](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) |
|
||||
12
3rdparty/discord-rpc/include/discord_register.h
vendored
Normal file
12
3rdparty/discord-rpc/include/discord_register.h
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
#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
|
||||
77
3rdparty/discord-rpc/include/discord_rpc.h
vendored
Normal file
77
3rdparty/discord-rpc/include/discord_rpc.h
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
#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
|
||||
41
3rdparty/discord-rpc/src/CMakeLists.txt
vendored
Normal file
41
3rdparty/discord-rpc/src/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
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)
|
||||
44
3rdparty/discord-rpc/src/backoff.h
vendored
Normal file
44
3rdparty/discord-rpc/src/backoff.h
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
#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
|
||||
22
3rdparty/discord-rpc/src/connection.h
vendored
Normal file
22
3rdparty/discord-rpc/src/connection.h
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
#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
|
||||
122
3rdparty/discord-rpc/src/connection_unix.cpp
vendored
Normal file
122
3rdparty/discord-rpc/src/connection_unix.cpp
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
#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
|
||||
|
||||
126
3rdparty/discord-rpc/src/connection_win.cpp
vendored
Normal file
126
3rdparty/discord-rpc/src/connection_win.cpp
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
#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
|
||||
|
||||
104
3rdparty/discord-rpc/src/discord_register_linux.cpp
vendored
Normal file
104
3rdparty/discord-rpc/src/discord_register_linux.cpp
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
80
3rdparty/discord-rpc/src/discord_register_osx.m
vendored
Normal file
80
3rdparty/discord-rpc/src/discord_register_osx.m
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
#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);
|
||||
}
|
||||
183
3rdparty/discord-rpc/src/discord_register_win.cpp
vendored
Normal file
183
3rdparty/discord-rpc/src/discord_register_win.cpp
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
479
3rdparty/discord-rpc/src/discord_rpc.cpp
vendored
Normal file
479
3rdparty/discord-rpc/src/discord_rpc.cpp
vendored
Normal file
@@ -0,0 +1,479 @@
|
||||
#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
|
||||
|
||||
40
3rdparty/discord-rpc/src/msg_queue.h
vendored
Normal file
40
3rdparty/discord-rpc/src/msg_queue.h
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
#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
|
||||
133
3rdparty/discord-rpc/src/rpc_connection.cpp
vendored
Normal file
133
3rdparty/discord-rpc/src/rpc_connection.cpp
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
#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
|
||||
|
||||
64
3rdparty/discord-rpc/src/rpc_connection.h
vendored
Normal file
64
3rdparty/discord-rpc/src/rpc_connection.h
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
#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
|
||||
|
||||
249
3rdparty/discord-rpc/src/serialization.cpp
vendored
Normal file
249
3rdparty/discord-rpc/src/serialization.cpp
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
#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
|
||||
|
||||
183
3rdparty/discord-rpc/src/serialization.h
vendored
Normal file
183
3rdparty/discord-rpc/src/serialization.h
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
#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
|
||||
11
3rdparty/kdsingleapplication/CMakeLists.txt
vendored
11
3rdparty/kdsingleapplication/CMakeLists.txt
vendored
@@ -1,11 +0,0 @@
|
||||
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)
|
||||
Submodule 3rdparty/kdsingleapplication/KDSingleApplication deleted from cb0c664b40
101
CMakeLists.txt
101
CMakeLists.txt
@@ -81,7 +81,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if(MSVC)
|
||||
list(APPEND COMPILE_OPTIONS /MP)
|
||||
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
|
||||
else()
|
||||
list(APPEND COMPILE_OPTIONS
|
||||
$<$<COMPILE_LANGUAGE:C>:-std=c11>
|
||||
@@ -128,7 +128,10 @@ add_definitions(
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
add_definitions(-DUNICODE)
|
||||
add_definitions(
|
||||
-DUNICODE
|
||||
-DNOMINMAX
|
||||
)
|
||||
endif()
|
||||
|
||||
if(BUILD_WERROR)
|
||||
@@ -207,34 +210,31 @@ endif()
|
||||
|
||||
find_package(GTest)
|
||||
|
||||
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
|
||||
|
||||
find_package(RapidJSON)
|
||||
|
||||
set(QT_VERSION_MAJOR 6)
|
||||
set(QT_MIN_VERSION 6.4.0)
|
||||
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
|
||||
set(QT_COMPONENTS Core Concurrent Gui Widgets Network Sql)
|
||||
set(QT_OPTIONAL_COMPONENTS LinguistTools Test)
|
||||
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test)
|
||||
if(UNIX AND NOT APPLE)
|
||||
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
|
||||
endif()
|
||||
set(QT_NO_PRIVATE_MODULE_WARNING ON)
|
||||
|
||||
find_package(Qt${QT_VERSION_MAJOR} ${QT_MIN_VERSION} COMPONENTS ${QT_COMPONENTS} REQUIRED OPTIONAL_COMPONENTS ${QT_OPTIONAL_COMPONENTS})
|
||||
|
||||
if(TARGET "Qt${QT_VERSION_MAJOR}::GuiPrivate")
|
||||
set(QT_GUI_PRIVATE_FOUND ON)
|
||||
endif()
|
||||
|
||||
if(Qt${QT_VERSION_MAJOR}DBus_FOUND)
|
||||
set(DBUS_FOUND ON)
|
||||
endif()
|
||||
|
||||
if(X11_FOUND)
|
||||
|
||||
find_path(QPA_QPLATFORMNATIVEINTERFACE_H qpa/qplatformnativeinterface.h PATHS ${Qt${QT_VERSION_MAJOR}Gui_INCLUDE_DIRS} ${Qt${QT_VERSION_MAJOR}Gui_PRIVATE_INCLUDE_DIRS})
|
||||
if(NOT QPA_QPLATFORMNATIVEINTERFACE_H)
|
||||
find_path(QPA_QPLATFORMNATIVEINTERFACE_H ${Qt${QT_VERSION_MAJOR}Gui_VERSION}/QtGui/qpa/qplatformnativeinterface.h PATHS ${Qt${QT_VERSION_MAJOR}Gui_INCLUDE_DIRS} ${Qt${QT_VERSION_MAJOR}Gui_PRIVATE_INCLUDE_DIRS})
|
||||
endif()
|
||||
if(QPA_QPLATFORMNATIVEINTERFACE_H)
|
||||
set(HAVE_QPA_QPLATFORMNATIVEINTERFACE_H ON)
|
||||
message(STATUS "Have qpa/qplatformnativeinterface.h header.")
|
||||
else()
|
||||
message(STATUS "Missing qpa/qplatformnativeinterface.h header.")
|
||||
endif()
|
||||
|
||||
# Check for QX11Application (Qt 6 compiled with XCB).
|
||||
set(CMAKE_REQUIRED_FLAGS "-std=c++17")
|
||||
set(CMAKE_REQUIRED_LIBRARIES Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui)
|
||||
@@ -249,23 +249,9 @@ if(X11_FOUND)
|
||||
)
|
||||
unset(CMAKE_REQUIRED_FLAGS)
|
||||
unset(CMAKE_REQUIRED_LIBRARIES)
|
||||
|
||||
endif()
|
||||
|
||||
# SingleApplication
|
||||
set(KDSINGLEAPPLICATION_NAME "KDSingleApplication-qt${QT_VERSION_MAJOR}")
|
||||
find_package(${KDSINGLEAPPLICATION_NAME} 1.1.0)
|
||||
if(TARGET KDAB::kdsingleapplication)
|
||||
set(KDSINGLEAPPLICATION_VERSION "${KDSingleApplication-qt6_VERSION}")
|
||||
message(STATUS "Using system KDSingleApplication (Version ${KDSINGLEAPPLICATION_VERSION})")
|
||||
set(SINGLEAPPLICATION_LIBRARIES KDAB::kdsingleapplication)
|
||||
else()
|
||||
message(STATUS "Using 3rdparty KDSingleApplication")
|
||||
add_subdirectory(3rdparty/kdsingleapplication)
|
||||
set(SINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/kdsingleapplication/KDSingleApplication/src)
|
||||
set(SINGLEAPPLICATION_LIBRARIES kdsingleapplication)
|
||||
add_definitions(-DKDSINGLEAPPLICATION_STATIC_BUILD)
|
||||
endif()
|
||||
find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
|
||||
|
||||
if(APPLE)
|
||||
find_library(SPARKLE Sparkle)
|
||||
@@ -372,6 +358,18 @@ if(APPLE OR WIN32)
|
||||
)
|
||||
endif()
|
||||
|
||||
optional_component(QPA_QPLATFORMNATIVEINTERFACE ON "QPA Platform Native Interface"
|
||||
DEPENDS "Qt Gui Private" QT_GUI_PRIVATE_FOUND
|
||||
)
|
||||
|
||||
optional_component(STREAMTAGREADER ON "Stream tagreader"
|
||||
DEPENDS "sparsehash" LIBSPARSEHASH_FOUND
|
||||
)
|
||||
|
||||
optional_component(DISCORD_RPC ON "Discord Rich Presence"
|
||||
DEPENDS "RapidJSON" RapidJSON_FOUND
|
||||
)
|
||||
|
||||
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
|
||||
set(HAVE_CHROMAPRINT ON)
|
||||
endif()
|
||||
@@ -458,6 +456,9 @@ set(SOURCES
|
||||
src/core/songmimedata.cpp
|
||||
src/core/platforminterface.cpp
|
||||
src/core/standardpaths.cpp
|
||||
src/core/httpbaserequest.cpp
|
||||
src/core/jsonbaserequest.cpp
|
||||
src/core/oauthenticator.cpp
|
||||
|
||||
src/utilities/strutils.cpp
|
||||
src/utilities/envutils.cpp
|
||||
@@ -478,6 +479,7 @@ set(SOURCES
|
||||
src/utilities/screenutils.cpp
|
||||
src/utilities/textencodingutils.cpp
|
||||
src/utilities/coveroptions.cpp
|
||||
src/utilities/musixmatchprovider.cpp
|
||||
|
||||
src/tagreader/tagreaderclient.cpp
|
||||
src/tagreader/tagreaderresult.cpp
|
||||
@@ -587,6 +589,8 @@ set(SOURCES
|
||||
src/playlist/playlistfilter.cpp
|
||||
src/playlist/playlistheader.cpp
|
||||
src/playlist/playlistitem.cpp
|
||||
src/playlist/songplaylistitem.cpp
|
||||
src/playlist/streamplaylistitem.cpp
|
||||
src/playlist/playlistitemmimedata.cpp
|
||||
src/playlist/playlistlistcontainer.cpp
|
||||
src/playlist/playlistlistmodel.cpp
|
||||
@@ -600,7 +604,6 @@ set(SOURCES
|
||||
src/playlist/playlistview.cpp
|
||||
src/playlist/playlistproxystyle.cpp
|
||||
src/playlist/songloaderinserter.cpp
|
||||
src/playlist/songplaylistitem.cpp
|
||||
src/playlist/dynamicplaylistcontrols.cpp
|
||||
src/playlist/playlistundocommandbase.cpp
|
||||
src/playlist/playlistundocommandinsertitems.cpp
|
||||
@@ -688,8 +691,6 @@ set(SOURCES
|
||||
src/lyrics/letraslyricsprovider.cpp
|
||||
src/lyrics/lyricfindlyricsprovider.cpp
|
||||
|
||||
src/providers/musixmatchprovider.cpp
|
||||
|
||||
src/settings/settingsdialog.cpp
|
||||
src/settings/settingspage.cpp
|
||||
src/settings/settingsitemdelegate.cpp
|
||||
@@ -752,7 +753,7 @@ set(SOURCES
|
||||
|
||||
src/streaming/streamingservices.cpp
|
||||
src/streaming/streamingservice.cpp
|
||||
src/streaming/streamplaylistitem.cpp
|
||||
src/streaming/streamserviceplaylistitem.cpp
|
||||
src/streaming/streamingsearchview.cpp
|
||||
src/streaming/streamingsearchmodel.cpp
|
||||
src/streaming/streamingsearchsortmodel.cpp
|
||||
@@ -770,7 +771,7 @@ set(SOURCES
|
||||
src/radios/radioview.cpp
|
||||
src/radios/radioviewcontainer.cpp
|
||||
src/radios/radioservice.cpp
|
||||
src/radios/radioplaylistitem.cpp
|
||||
src/radios/radiostreamplaylistitem.cpp
|
||||
src/radios/radiochannel.cpp
|
||||
src/radios/somafmservice.cpp
|
||||
src/radios/radioparadiseservice.cpp
|
||||
@@ -852,6 +853,9 @@ set(HEADERS
|
||||
src/core/stylesheetloader.h
|
||||
src/core/localredirectserver.h
|
||||
src/core/songmimedata.h
|
||||
src/core/httpbaserequest.h
|
||||
src/core/jsonbaserequest.h
|
||||
src/core/oauthenticator.h
|
||||
|
||||
src/tagreader/tagreaderclient.h
|
||||
src/tagreader/tagreaderreply.h
|
||||
@@ -1234,6 +1238,16 @@ optional_source(WIN32
|
||||
src/core/windows7thumbbar.h
|
||||
)
|
||||
|
||||
optional_source(HAVE_STREAMTAGREADER
|
||||
SOURCES src/tagreader/streamtagreader.cpp src/tagreader/tagreaderreadstreamrequest.cpp src/tagreader/tagreaderreadstreamreply.cpp
|
||||
HEADERS src/tagreader/tagreaderreadstreamreply.h
|
||||
)
|
||||
|
||||
optional_source(HAVE_DISCORD_RPC
|
||||
SOURCES src/discord/richpresence.cpp
|
||||
HEADERS src/discord/richpresence.h
|
||||
)
|
||||
|
||||
if(HAVE_GLOBALSHORTCUTS)
|
||||
|
||||
optional_source(HAVE_GLOBALSHORTCUTS
|
||||
@@ -1478,6 +1492,11 @@ if(LINUX AND LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
|
||||
add_subdirectory(debian)
|
||||
endif()
|
||||
|
||||
if(HAVE_DISCORD_RPC)
|
||||
add_subdirectory(3rdparty/discord-rpc)
|
||||
target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc/include)
|
||||
endif()
|
||||
|
||||
if(HAVE_TRANSLATIONS)
|
||||
qt_add_lupdate(strawberry_lib TS_FILES "${CMAKE_SOURCE_DIR}/src/translations/strawberry_en_US.ts" OPTIONS -locations none -no-ui-lines -no-obsolete)
|
||||
file(GLOB_RECURSE ts_files ${CMAKE_SOURCE_DIR}/src/translations/*.ts)
|
||||
@@ -1499,10 +1518,6 @@ if(SINGLEAPPLICATION_INCLUDE_DIRS)
|
||||
target_include_directories(strawberry_lib SYSTEM PUBLIC ${SINGLEAPPLICATION_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
if(HAVE_QPA_QPLATFORMNATIVEINTERFACE_H)
|
||||
target_include_directories(strawberry_lib SYSTEM PUBLIC ${Qt${QT_VERSION_MAJOR}Gui_PRIVATE_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
target_link_libraries(strawberry_lib PUBLIC
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
$<$<BOOL:${HAVE_BACKTRACE}>:${Backtrace_LIBRARIES}>
|
||||
@@ -1523,8 +1538,10 @@ target_link_libraries(strawberry_lib PUBLIC
|
||||
Qt${QT_VERSION_MAJOR}::Network
|
||||
Qt${QT_VERSION_MAJOR}::Sql
|
||||
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
|
||||
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
|
||||
ICU::uc
|
||||
ICU::i18n
|
||||
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>
|
||||
$<$<BOOL:${HAVE_ALSA}>:ALSA::ALSA>
|
||||
$<$<BOOL:${HAVE_PULSE}>:PkgConfig::LIBPULSE>
|
||||
$<$<BOOL:${HAVE_CHROMAPRINT}>:PkgConfig::CHROMAPRINT>
|
||||
@@ -1539,7 +1556,7 @@ target_link_libraries(strawberry_lib PUBLIC
|
||||
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
|
||||
$<$<BOOL:${WIN32}>:dsound dwmapi getopt-win::getopt>
|
||||
$<$<BOOL:${MSVC}>:WindowsApp>
|
||||
${SINGLEAPPLICATION_LIBRARIES}
|
||||
KDAB::kdsingleapplication
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
@@ -1558,6 +1575,10 @@ if(APPLE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(HAVE_DISCORD_RPC)
|
||||
target_link_libraries(strawberry_lib PRIVATE discord-rpc)
|
||||
endif()
|
||||
|
||||
target_link_libraries(strawberry PUBLIC strawberry_lib)
|
||||
|
||||
if(NOT APPLE)
|
||||
|
||||
42
Changelog
42
Changelog
@@ -2,6 +2,48 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
Version 1.2.9 (2025.04.08):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed subsonic parse error (#1719).
|
||||
* Fixed Deezer cover provider parse error (#1716).
|
||||
* Fixed last.fm import progress.
|
||||
* (Windows|MinGW) Switched from winpthreads to win32 threads, winpthreads are no longer working with Qt as of version 6.9 (QTBUG-131892).
|
||||
|
||||
Enhancements:
|
||||
* Added option to disable playbin3.
|
||||
|
||||
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:
|
||||
|
||||
25
README.md
25
README.md
@@ -4,7 +4,7 @@
|
||||
[](https://patreon.com/jonaskvinge)
|
||||
[](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 toolkit.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
@@ -59,7 +59,9 @@ 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/)
|
||||
* Subsonic, Tidal, Spotify and Qobuz streaming support
|
||||
* Streaming from Subsonic compatible servers
|
||||
* Unofficial Tidal, Spotify and Qobuz integration
|
||||
* Discord rich presence
|
||||
|
||||
|
||||
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
|
||||
@@ -70,7 +72,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](https://cmake.org/)
|
||||
* [CMake 3.13 or higher](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/)
|
||||
@@ -81,7 +83,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](https://github.com/KDAB/KDSingleApplication)
|
||||
* [KDSingleApplication 1.1.0 or higher](https://github.com/KDAB/KDSingleApplication)
|
||||
|
||||
Optional dependencies:
|
||||
|
||||
@@ -92,25 +94,24 @@ 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: Compiling from source
|
||||
### :wrench: Build from source
|
||||
|
||||
### Get the code:
|
||||
|
||||
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
|
||||
|
||||
### Compile and install:
|
||||
### Build and install:
|
||||
|
||||
cd strawberry
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make -j $(nproc)
|
||||
sudo make install
|
||||
cmake -S . -B build
|
||||
cmake --build build --parallel $(nproc)
|
||||
sudo cmake --install build
|
||||
|
||||
To compile on Windows with Visual Studio 2019 or 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
To build on Windows with Visual Studio 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
|
||||
### :penguin: Packaging status
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 2)
|
||||
set(STRAWBERRY_VERSION_PATCH 6)
|
||||
set(STRAWBERRY_VERSION_PATCH 9)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
|
||||
7
debian/control
vendored
7
debian/control
vendored
@@ -15,11 +15,14 @@ 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,
|
||||
@@ -27,7 +30,9 @@ Build-Depends: debhelper-compat (= 12),
|
||||
libmtp-dev,
|
||||
libchromaprint-dev,
|
||||
libfftw3-dev,
|
||||
libebur128-dev
|
||||
libebur128-dev,
|
||||
libsparsehash-dev,
|
||||
rapidjson-dev
|
||||
Standards-Version: 4.7.0
|
||||
|
||||
Package: strawberry
|
||||
|
||||
4
debian/rules
vendored
4
debian/rules
vendored
@@ -3,6 +3,10 @@
|
||||
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
|
||||
|
||||
|
||||
29
dist/scripts/import-from-clementine.sh
vendored
29
dist/scripts/import-from-clementine.sh
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
# Strawberry Music Player
|
||||
# Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
# 2021 Alexey Vazhnov
|
||||
# Copyright 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://github.com/strawberrymusicplayer/strawberry/wiki/Import-collection-library-and-playlists-data-from-Clementine
|
||||
# Based on https://wiki.strawberrymusicplayer.org/wiki/Import_collection_library_and_playlists_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 information** from Strawberry database!'
|
||||
read -r -p 'Do you want to continue? (the only YES is accepted) ' answer
|
||||
echo 'This script will **delete all data** from the Strawberry database!'
|
||||
read -r -p 'Do you want to continue? (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,
|
||||
filename,
|
||||
CASE WHEN filename IS NULL THEN '' ELSE filename END,
|
||||
filetype,
|
||||
filesize,
|
||||
mtime,
|
||||
@@ -162,16 +162,9 @@ 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 = "";
|
||||
|
||||
/* 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;
|
||||
UPDATE strawberry.playlist_items SET artist_id = '';
|
||||
UPDATE strawberry.playlist_items SET album_id = '';
|
||||
UPDATE strawberry.playlist_items SET song_id = '';
|
||||
|
||||
EOF
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
</screenshots>
|
||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||
<releases>
|
||||
<release version="1.2.9" date="2025-04-08"/>
|
||||
<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"/>
|
||||
|
||||
8
dist/unix/strawberry.spec.in
vendored
8
dist/unix/strawberry.spec.in
vendored
@@ -63,8 +63,14 @@ 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
|
||||
@@ -96,7 +102,7 @@ Features:
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
- Unofficial streaming support for Tidal and Qobuz
|
||||
|
||||
%if 0%{?suse_version}
|
||||
%if 0%{?suse_version} && 0%{?suse_version} <= 1600
|
||||
%debug_package
|
||||
%endif
|
||||
|
||||
|
||||
34
dist/windows/strawberry.nsi.in
vendored
34
dist/windows/strawberry.nsi.in
vendored
@@ -259,7 +259,7 @@ Section "Strawberry" Strawberry
|
||||
File "libssl-3-x64.dll"
|
||||
!endif
|
||||
|
||||
File "libFLAC-12.dll"
|
||||
File "libFLAC-14.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-0.dll"
|
||||
File "libsqlite3.dll"
|
||||
File "libssp-0.dll"
|
||||
File "libstdc++-6.dll"
|
||||
File "libtag.dll"
|
||||
@@ -458,11 +458,11 @@ Section "Strawberry" Strawberry
|
||||
|
||||
; Common files
|
||||
|
||||
File "icudt76.dll"
|
||||
File "icudt77.dll"
|
||||
File "libfftw3-3.dll"
|
||||
!ifdef msvc && debug
|
||||
File "icuin76d.dll"
|
||||
File "icuuc76d.dll"
|
||||
File "icuin77d.dll"
|
||||
File "icuuc77d.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 "icuin76.dll"
|
||||
File "icuuc76.dll"
|
||||
File "icuin77.dll"
|
||||
File "icuuc77.dll"
|
||||
File "libxml2.dll"
|
||||
File "Qt6Concurrent.dll"
|
||||
File "Qt6Core.dll"
|
||||
@@ -596,6 +596,7 @@ 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"
|
||||
@@ -719,7 +720,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
|
||||
|
||||
@@ -783,7 +784,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libssl-3-x64.dll"
|
||||
!endif
|
||||
|
||||
Delete "$INSTDIR\libFLAC-12.dll"
|
||||
Delete "$INSTDIR\libFLAC-14.dll"
|
||||
Delete "$INSTDIR\libbrotlicommon.dll"
|
||||
Delete "$INSTDIR\libbrotlidec.dll"
|
||||
Delete "$INSTDIR\libbrotlienc.dll"
|
||||
@@ -848,7 +849,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libqtsparkle-qt6.dll"
|
||||
Delete "$INSTDIR\libsoup-3.0-0.dll"
|
||||
Delete "$INSTDIR\libspeex-1.dll"
|
||||
Delete "$INSTDIR\libsqlite3-0.dll"
|
||||
Delete "$INSTDIR\libsqlite3.dll"
|
||||
Delete "$INSTDIR\libssp-0.dll"
|
||||
Delete "$INSTDIR\libstdc++-6.dll"
|
||||
Delete "$INSTDIR\libtag.dll"
|
||||
@@ -981,11 +982,11 @@ Section "Uninstall"
|
||||
|
||||
; Common files
|
||||
|
||||
Delete "$INSTDIR\icudt76.dll"
|
||||
Delete "$INSTDIR\icudt77.dll"
|
||||
Delete "$INSTDIR\libfftw3-3.dll"
|
||||
!ifdef msvc && debug
|
||||
Delete "$INSTDIR\icuin76d.dll"
|
||||
Delete "$INSTDIR\icuuc76d.dll"
|
||||
Delete "$INSTDIR\icuin77d.dll"
|
||||
Delete "$INSTDIR\icuuc77d.dll"
|
||||
Delete "$INSTDIR\libxml2d.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrentd.dll"
|
||||
Delete "$INSTDIR\Qt6Cored.dll"
|
||||
@@ -994,8 +995,8 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\Qt6Sqld.dll"
|
||||
Delete "$INSTDIR\Qt6Widgetsd.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\icuin76.dll"
|
||||
Delete "$INSTDIR\icuuc76.dll"
|
||||
Delete "$INSTDIR\icuin77.dll"
|
||||
Delete "$INSTDIR\icuuc77.dll"
|
||||
Delete "$INSTDIR\libxml2.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrent.dll"
|
||||
Delete "$INSTDIR\Qt6Core.dll"
|
||||
@@ -1052,6 +1053,7 @@ 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"
|
||||
@@ -1177,7 +1179,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
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@
|
||||
// Make an INSTRUCTIONS file
|
||||
// can't mod scope in analyze you have to use transform for 2D use setErasePixmap Qt function insetead of m_background
|
||||
|
||||
AnalyzerBase::AnalyzerBase(QWidget *parent, const uint scopeSize)
|
||||
AnalyzerBase::AnalyzerBase(QWidget *parent, const uint scope_size)
|
||||
: QWidget(parent),
|
||||
fht_(new FHT(scopeSize)),
|
||||
fht_(new FHT(scope_size)),
|
||||
engine_(nullptr),
|
||||
lastscope_(512),
|
||||
new_frame_(false),
|
||||
@@ -100,7 +100,7 @@ void AnalyzerBase::transform(Scope &scope) {
|
||||
fht_->logSpectrum(scope.data(), aux.data());
|
||||
fht_->scale(scope.data(), 1.0F / 20);
|
||||
|
||||
scope.resize(fht_->size() / 2); // second half of values are rubbish
|
||||
scope.resize(static_cast<size_t>(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_);
|
||||
int i = 0;
|
||||
size_t 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(fht_->size());
|
||||
lastscope_.resize(static_cast<size_t>(fht_->size()));
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ int AnalyzerBase::resizeExponent(int exp) {
|
||||
|
||||
if (exp != fht_->sizeExp()) {
|
||||
delete fht_;
|
||||
fht_ = new FHT(exp);
|
||||
fht_ = new FHT(static_cast<uint>(exp));
|
||||
}
|
||||
return exp;
|
||||
|
||||
@@ -211,28 +211,28 @@ void AnalyzerBase::demo(QPainter &p) {
|
||||
|
||||
}
|
||||
|
||||
void AnalyzerBase::interpolate(const Scope &inVec, Scope &outVec) {
|
||||
void AnalyzerBase::interpolate(const Scope &in_scope, Scope &out_scope) {
|
||||
|
||||
double pos = 0.0;
|
||||
const double step = static_cast<double>(inVec.size()) / static_cast<double>(outVec.size());
|
||||
const double step = static_cast<double>(in_scope.size()) / static_cast<double>(out_scope.size());
|
||||
|
||||
for (uint i = 0; i < outVec.size(); ++i, pos += step) {
|
||||
for (uint i = 0; i < out_scope.size(); ++i, pos += step) {
|
||||
const double error = pos - std::floor(pos);
|
||||
const uint64_t offset = static_cast<uint64_t>(pos);
|
||||
|
||||
uint64_t indexLeft = offset + 0;
|
||||
|
||||
if (indexLeft >= inVec.size()) {
|
||||
indexLeft = inVec.size() - 1;
|
||||
if (indexLeft >= in_scope.size()) {
|
||||
indexLeft = in_scope.size() - 1;
|
||||
}
|
||||
|
||||
uint64_t indexRight = offset + 1;
|
||||
|
||||
if (indexRight >= inVec.size()) {
|
||||
indexRight = inVec.size() - 1;
|
||||
if (indexRight >= in_scope.size()) {
|
||||
indexRight = in_scope.size() - 1;
|
||||
}
|
||||
|
||||
outVec[i] = inVec[indexLeft] * (1.0F - static_cast<float>(error)) + inVec[indexRight] * static_cast<float>(error);
|
||||
out_scope[i] = in_scope[indexLeft] * (1.0F - static_cast<float>(error)) + in_scope[indexRight] * static_cast<float>(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class AnalyzerBase : public QWidget {
|
||||
|
||||
protected:
|
||||
using Scope = std::vector<float>;
|
||||
explicit AnalyzerBase(QWidget*, const uint scopeSize = 7);
|
||||
explicit AnalyzerBase(QWidget *parent, const uint scope_size = 7);
|
||||
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
void showEvent(QShowEvent *e) override;
|
||||
@@ -71,12 +71,12 @@ class AnalyzerBase : public QWidget {
|
||||
int resizeExponent(int exp);
|
||||
int resizeForBands(const int bands);
|
||||
virtual void init() {}
|
||||
virtual void transform(Scope&);
|
||||
virtual void analyze(QPainter &p, const Scope&, const bool new_frame) = 0;
|
||||
virtual void transform(Scope &scope);
|
||||
virtual void analyze(QPainter &p, const Scope &s, const bool new_frame) = 0;
|
||||
virtual void demo(QPainter &p);
|
||||
|
||||
void interpolate(const Scope&, Scope&);
|
||||
void initSin(Scope&, const uint = 6000);
|
||||
void interpolate(const Scope &in_scope, Scope &out_scope);
|
||||
void initSin(Scope &v, const uint size = 6000);
|
||||
|
||||
protected:
|
||||
QBasicTimer timer_;
|
||||
|
||||
@@ -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(columns_);
|
||||
scope_.resize(static_cast<size_t>(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 (int x = 0, y = 0; x < static_cast<int>(scope_.size()); ++x) {
|
||||
for (qint64 x = 0, y = 0; x < static_cast<qint64>(scope_.size()); ++x) {
|
||||
// determine y
|
||||
for (y = 0; scope_[x] < yscale_.at(y); ++y);
|
||||
for (y = 0; scope_[static_cast<quint64>(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] = y;
|
||||
store_[x] = static_cast<double>(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] = y;
|
||||
fade_pos_[x] = static_cast<int>(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(x * (kWidth + 1), y2, fade_bars_[offset], 0, 0, kWidth, height() - y2);
|
||||
canvas_painter.drawPixmap(static_cast<int>(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(x * (kWidth + 1), y * (kHeight + 1) + y_, *bar(), 0, y * (kHeight + 1), bar()->width(), bar()->height());
|
||||
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());
|
||||
}
|
||||
|
||||
for (int x = 0; x < store_.size(); ++x) {
|
||||
|
||||
@@ -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(bands_);
|
||||
scope_.resize(static_cast<size_t>(bands_));
|
||||
|
||||
F_ = static_cast<double>(HEIGHT) / (log10(256) * 1.1 /*<- max. amplitude*/);
|
||||
|
||||
@@ -112,17 +112,19 @@ void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame
|
||||
return;
|
||||
}
|
||||
|
||||
const uint MAX_HEIGHT = height() - 1;
|
||||
const uint MAX_HEIGHT = static_cast<uint>(height() - 1);
|
||||
|
||||
QPainter canvas_painter(&canvas_);
|
||||
canvas_.fill(palette().color(QPalette::Window));
|
||||
|
||||
interpolate(scope, scope_);
|
||||
|
||||
for (int i = 0, x = 0, y = 0; i < bands_; ++i, x += kColumnWidth + 1) {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
for (size_t i = 0; i < static_cast<size_t>(bands_); ++i, x += kColumnWidth + 1) {
|
||||
double h = log10(scope_[i] * 256.0) * F_;
|
||||
|
||||
if (h > MAX_HEIGHT) h = MAX_HEIGHT;
|
||||
h = std::min(h, static_cast<double>(MAX_HEIGHT));
|
||||
|
||||
if (h > bar_height_[i]) {
|
||||
bar_height_[i] = h;
|
||||
@@ -138,7 +140,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
|
||||
if (bar_height_[i] < 0.0) bar_height_[i] = 0.0;
|
||||
bar_height_[i] = std::max(0.0, bar_height_[i]);
|
||||
}
|
||||
|
||||
peak_handling:
|
||||
@@ -147,8 +149,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
|
||||
|
||||
if (peak_height_[i] < bar_height_[i]) peak_height_[i] = bar_height_[i];
|
||||
if (peak_height_[i] < 0.0) peak_height_[i] = 0.0;
|
||||
peak_height_[i] = std::max(bar_height_[i], bar_height_[i]);
|
||||
peak_height_[i] = std::max(0.0, peak_height_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
int sample = 0;
|
||||
size_t sample = 0;
|
||||
for (int band = 0; band < kRainbowBands; ++band) {
|
||||
float accumulator = 0.0;
|
||||
for (int i = 0; i < samples_per_band; ++i) {
|
||||
|
||||
@@ -85,10 +85,10 @@ void SonogramAnalyzer::transform(Scope &scope) {
|
||||
|
||||
fht_->power2(scope.data());
|
||||
fht_->scale(scope.data(), 1.0 / 256);
|
||||
scope.resize(fht_->size() / 2);
|
||||
scope.resize(static_cast<size_t>(fht_->size() / 2));
|
||||
|
||||
}
|
||||
|
||||
void SonogramAnalyzer::demo(QPainter &p) {
|
||||
analyze(p, Scope(fht_->size(), 0), new_frame_);
|
||||
analyze(p, Scope(static_cast<size_t>(fht_->size()), 0), new_frame_);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ void TurbineAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_fr
|
||||
return;
|
||||
}
|
||||
|
||||
const uint hd2 = height() / 2;
|
||||
const uint hd2 = static_cast<uint>(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
|
||||
if (bar_height_[i] < 0.0) bar_height_[i] = 0.0;
|
||||
bar_height_[i] = std::max(0.0, bar_height_[i]);
|
||||
}
|
||||
|
||||
peak_handling:
|
||||
|
||||
@@ -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 int num_samples = static_cast<int>(s.size());
|
||||
const size_t num_samples = static_cast<size_t>(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 (int i = 0; i < num_samples; ++i) {
|
||||
for (size_t 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(fht_->size(), 0), new_frame_);
|
||||
analyze(p, Scope(static_cast<size_t>(fht_->size()), 0), new_frame_);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -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_collection_song() && new_collection_directory_id) {
|
||||
if (updated_song.is_linked_collection_song() && new_collection_directory_id) {
|
||||
updated_song.set_directory_id(new_collection_directory_id.value());
|
||||
}
|
||||
|
||||
@@ -853,6 +853,10 @@ 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());
|
||||
@@ -879,6 +883,24 @@ 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());
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -234,6 +234,9 @@ 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);
|
||||
@@ -247,6 +250,7 @@ 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();
|
||||
|
||||
@@ -199,8 +199,8 @@ void CollectionLibrary::SyncPlaycountAndRatingToFiles() {
|
||||
task_manager_->SetTaskBlocksCollectionScans(task_id);
|
||||
|
||||
const SongList songs = backend_->GetAllSongs();
|
||||
const qint64 nb_songs = songs.size();
|
||||
int i = 0;
|
||||
const quint64 nb_songs = static_cast<quint64>(songs.size());
|
||||
quint64 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());
|
||||
|
||||
@@ -397,13 +397,13 @@ QMimeData *CollectionModel::mimeData(const QModelIndexList &indexes) const {
|
||||
GetChildSongs(IndexToItem(idx), songs, song_ids, urls);
|
||||
}
|
||||
|
||||
SongMimeData *data = new SongMimeData;
|
||||
data->setUrls(urls);
|
||||
data->backend = backend_;
|
||||
data->songs = songs;
|
||||
data->name_for_new_playlist_ = Song::GetNameForNewPlaylist(data->songs);
|
||||
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);
|
||||
|
||||
return data;
|
||||
return song_mime_data;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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 icon_disk_cache_->cacheSize(); }
|
||||
quint64 icon_disk_cache_size() { return static_cast<quint64>(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>());
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -30,41 +28,52 @@
|
||||
|
||||
class SqlRow;
|
||||
|
||||
CollectionPlaylistItem::CollectionPlaylistItem() : PlaylistItem(Song::Source::Collection) {
|
||||
song_.set_source(Song::Source::Collection);
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song::Source source) : PlaylistItem(source) {
|
||||
song_.set_source(source);
|
||||
}
|
||||
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(Song::Source::Collection), song_(song) {
|
||||
song_.set_source(Song::Source::Collection);
|
||||
}
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
|
||||
|
||||
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) {
|
||||
|
||||
// Rows from the songs tables come first
|
||||
song_.InitFromQuery(query, true);
|
||||
song_.set_source(Song::Source::Collection);
|
||||
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);
|
||||
|
||||
return song_.is_valid();
|
||||
|
||||
}
|
||||
|
||||
QVariant CollectionPlaylistItem::DatabaseValue(DatabaseColumn column) const {
|
||||
void CollectionPlaylistItem::Reload() {
|
||||
|
||||
switch (column) {
|
||||
case Column_CollectionId: return song_.id();
|
||||
default: return PlaylistItem::DatabaseValue(column);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -34,9 +32,11 @@ class SqlRow;
|
||||
|
||||
class CollectionPlaylistItem : public PlaylistItem {
|
||||
public:
|
||||
explicit CollectionPlaylistItem();
|
||||
explicit CollectionPlaylistItem(const Song::Source source);
|
||||
explicit CollectionPlaylistItem(const Song &song);
|
||||
|
||||
QUrl Url() const override;
|
||||
|
||||
bool InitFromQuery(const SqlRow &query) override;
|
||||
void Reload() override;
|
||||
|
||||
@@ -44,15 +44,13 @@ 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(DatabaseColumn column) const override;
|
||||
Song DatabaseSongMetadata() const override { return Song(Song::Source::Collection); }
|
||||
QVariant DatabaseValue(const DatabaseColumn database_column) const override;
|
||||
Song DatabaseSongMetadata() const override { return Song(source_); }
|
||||
|
||||
protected:
|
||||
Song song_;
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
#include "collectionitem.h"
|
||||
#include "collectionitemdelegate.h"
|
||||
#include "collectionview.h"
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
# include "device/devicemanager.h"
|
||||
# include "device/devicestatefiltermodel.h"
|
||||
#endif
|
||||
@@ -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_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
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_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
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_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
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_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
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_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
|
||||
#endif
|
||||
|
||||
@@ -759,7 +759,7 @@ void CollectionView::RescanSongs() {
|
||||
|
||||
void CollectionView::CopyToDevice() {
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
if (!organize_dialog_) {
|
||||
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ class CollectionView : public AutoExpandingTreeView {
|
||||
QAction *action_organize_;
|
||||
QAction *action_search_for_this_;
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
QAction *action_copy_to_device_;
|
||||
#endif
|
||||
QAction *action_edit_track_;
|
||||
|
||||
@@ -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(song.beginning_nanosec(), song);
|
||||
sections_map.insert(static_cast<quint64>(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(new_cue_song.beginning_nanosec())) { // Changed section
|
||||
const Song matching_cue_song = sections_map[new_cue_song.beginning_nanosec()];
|
||||
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())];
|
||||
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() ? cue_last_modified.toSecsSinceEpoch() : 0;
|
||||
return cue_last_modified.isValid() ? static_cast<quint64>(cue_last_modified.toSecsSinceEpoch()) : 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@
|
||||
#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
|
||||
@@ -41,8 +43,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
|
||||
|
||||
|
||||
@@ -26,10 +26,14 @@ namespace BackendSettings {
|
||||
|
||||
constexpr char kSettingsGroup[] = "Backend";
|
||||
|
||||
constexpr char kEngine[] = "Engine";
|
||||
constexpr char kOutput[] = "Output";
|
||||
constexpr char kDevice[] = "Device";
|
||||
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 kALSAPlugin[] = "alsaplugin";
|
||||
constexpr char kPlaybin3[] = "playbin3";
|
||||
constexpr char kExclusiveMode[] = "exclusive_mode";
|
||||
constexpr char kVolumeControl[] = "volume_control";
|
||||
constexpr char kChannelsEnabled[] = "channels_enabled";
|
||||
|
||||
@@ -65,4 +65,12 @@ constexpr QRgb kPresetRed = qRgb(202, 22, 16);
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace DiscordRPCSettings {
|
||||
|
||||
constexpr char kSettingsGroup[] = "DiscordRPC";
|
||||
|
||||
constexpr char kEnabled[] = "enabled";
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // NOTIFICATIONSSETTINGS_H
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -36,6 +36,7 @@
|
||||
#include <QMetaObject>
|
||||
#include <QCoreApplication>
|
||||
#include <QAbstractEventDispatcher>
|
||||
#include <QTimer>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
@@ -208,7 +209,7 @@ class ApplicationImpl {
|
||||
scrobbler->AddService(make_shared<LibreFMScrobbler>(scrobbler->settings(), app->network()));
|
||||
scrobbler->AddService(make_shared<ListenBrainzScrobbler>(scrobbler->settings(), app->network()));
|
||||
#ifdef HAVE_SUBSONIC
|
||||
scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->streaming_services()->Service<SubsonicService>(), app));
|
||||
scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->network(), app->streaming_services()->Service<SubsonicService>(), app));
|
||||
#endif
|
||||
return scrobbler;
|
||||
}),
|
||||
|
||||
@@ -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, page_count - sqlite3_backup_remaining(backup), page_count);
|
||||
task_manager_->SetTaskProgress(task_id, static_cast<quint64>(page_count - sqlite3_backup_remaining(backup)), static_cast<quint64>(page_count));
|
||||
}
|
||||
while (ret == SQLITE_OK);
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ void DeleteFiles::ProcessSomeFiles() {
|
||||
|
||||
// None left?
|
||||
if (progress_ >= songs_.count()) {
|
||||
task_manager_->SetTaskProgress(task_id_, progress_, songs_.count());
|
||||
task_manager_->SetTaskProgress(task_id_, static_cast<quint64>(progress_), static_cast<quint64>(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_, progress_, songs_.count());
|
||||
task_manager_->SetTaskProgress(task_id_, static_cast<quint64>(progress_), static_cast<quint64>(songs_.count()));
|
||||
|
||||
const Song song = songs_.value(progress_);
|
||||
|
||||
|
||||
@@ -113,7 +113,11 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
181
src/core/httpbaserequest.cpp
Normal file
181
src/core/httpbaserequest.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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 ¶ms, 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 ¶m : 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 ¶ms) {
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : 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;
|
||||
}
|
||||
|
||||
}
|
||||
110
src/core/httpbaserequest.h
Normal file
110
src/core/httpbaserequest.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 ¶ms = 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 ¶ms);
|
||||
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
|
||||
@@ -43,7 +43,7 @@ bool IconLoader::custom_icons_ = false;
|
||||
|
||||
void IconLoader::Init() {
|
||||
|
||||
#if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN)
|
||||
#if !defined(Q_OS_MACOS) && !defined(Q_OS_WIN32)
|
||||
Settings s;
|
||||
s.beginGroup(AppearanceSettings::kSettingsGroup);
|
||||
system_icons_ = s.value("system_icons", false).toBool();
|
||||
|
||||
103
src/core/jsonbaserequest.cpp
Normal file
103
src/core/jsonbaserequest.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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();
|
||||
|
||||
}
|
||||
68
src/core/jsonbaserequest.h
Normal file
68
src/core/jsonbaserequest.h
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* This file was part of Clementine.
|
||||
* Strawberry Music Player
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -44,7 +43,8 @@ using namespace Qt::Literals::StringLiterals;
|
||||
LocalRedirectServer::LocalRedirectServer(QObject *parent)
|
||||
: QTcpServer(parent),
|
||||
port_(0),
|
||||
socket_(nullptr) {}
|
||||
socket_(nullptr),
|
||||
success_(false) {}
|
||||
|
||||
LocalRedirectServer::~LocalRedirectServer() {
|
||||
if (isListening()) close();
|
||||
@@ -52,7 +52,8 @@ LocalRedirectServer::~LocalRedirectServer() {
|
||||
|
||||
bool LocalRedirectServer::Listen() {
|
||||
|
||||
if (!listen(QHostAddress::LocalHost, port_)) {
|
||||
if (!listen(QHostAddress::LocalHost, static_cast<quint64>(port_))) {
|
||||
success_ = false;
|
||||
error_ = errorString();
|
||||
return false;
|
||||
}
|
||||
@@ -61,6 +62,7 @@ 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;
|
||||
@@ -77,7 +79,7 @@ void LocalRedirectServer::NewConnection() {
|
||||
|
||||
void LocalRedirectServer::incomingConnection(qintptr socket_descriptor) {
|
||||
|
||||
if (socket_) {
|
||||
if (socket_ != nullptr) {
|
||||
if (socket_->state() == QAbstractSocket::ConnectedState) socket_->close();
|
||||
socket_->deleteLater();
|
||||
socket_ = nullptr;
|
||||
@@ -88,6 +90,7 @@ 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;
|
||||
@@ -115,6 +118,10 @@ 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();
|
||||
}
|
||||
@@ -169,9 +176,9 @@ QUrl LocalRedirectServer::ParseUrlFromRequest(const QByteArray &request) const {
|
||||
|
||||
const QByteArrayList lines = request.split('\r');
|
||||
const QByteArray &request_line = lines[0];
|
||||
QByteArray path = request_line.split(' ')[1];
|
||||
QUrl base_url = url_;
|
||||
QUrl request_url(base_url.toString() + QString::fromLatin1(path.mid(1)), QUrl::StrictMode);
|
||||
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);
|
||||
|
||||
return request_url;
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* This file was part of Clementine.
|
||||
* Strawberry Music Player
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -39,12 +38,16 @@ 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();
|
||||
|
||||
@@ -66,6 +69,7 @@ class LocalRedirectServer : public QTcpServer {
|
||||
QUrl request_url_;
|
||||
QAbstractSocket *socket_;
|
||||
QByteArray buffer_;
|
||||
bool success_;
|
||||
QString error_;
|
||||
};
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
#include "lyrics/lyricsproviders.h"
|
||||
#include "device/devicemanager.h"
|
||||
#include "device/devicestatefiltermodel.h"
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
# include "device/deviceview.h"
|
||||
# include "device/deviceviewcontainer.h"
|
||||
#endif
|
||||
@@ -208,7 +208,7 @@
|
||||
|
||||
#include "organize/organizeerrordialog.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#ifdef Q_OS_WIN32
|
||||
# include "core/windows7thumbbar.h"
|
||||
#endif
|
||||
|
||||
@@ -228,6 +228,10 @@
|
||||
# include <qtsparkle-qt6/Updater>
|
||||
#endif // HAVE_QTSPARKLE
|
||||
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
#include "discord/richpresence.h"
|
||||
#endif
|
||||
|
||||
using std::make_unique;
|
||||
using std::make_shared;
|
||||
using namespace std::chrono_literals;
|
||||
@@ -275,15 +279,24 @@ constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-
|
||||
} // namespace
|
||||
#endif // HAVE_QTSPARKLE
|
||||
|
||||
MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd, const CommandlineOptions &options, QWidget *parent)
|
||||
MainWindow::MainWindow(Application *app,
|
||||
SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd,
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence,
|
||||
#endif
|
||||
const CommandlineOptions &options,
|
||||
QWidget *parent)
|
||||
: QMainWindow(parent),
|
||||
ui_(new Ui_MainWindow),
|
||||
#ifdef Q_OS_WIN
|
||||
#ifdef Q_OS_WIN32
|
||||
thumbbar_(new Windows7ThumbBar(this)),
|
||||
#endif
|
||||
app_(app),
|
||||
tray_icon_(tray_icon),
|
||||
osd_(osd),
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord_rich_presence_(discord_rich_presence),
|
||||
#endif
|
||||
console_([app, this]() {
|
||||
Console *console = new Console(app->database());
|
||||
QObject::connect(console, &Console::Error, this, &MainWindow::ShowErrorDialog);
|
||||
@@ -297,7 +310,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
context_view_(new ContextView(this)),
|
||||
collection_view_(new CollectionViewContainer(this)),
|
||||
file_view_(new FileView(this)),
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
device_view_(new DeviceViewContainer(this)),
|
||||
#endif
|
||||
playlist_list_(new PlaylistListContainer(this)),
|
||||
@@ -362,7 +375,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
playlist_move_to_collection_(nullptr),
|
||||
playlist_open_in_browser_(nullptr),
|
||||
playlist_organize_(nullptr),
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
playlist_copy_to_device_(nullptr),
|
||||
#endif
|
||||
playlist_delete_(nullptr),
|
||||
@@ -388,6 +401,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
was_minimized_(false),
|
||||
exit_(false),
|
||||
exit_count_(0),
|
||||
playlists_loaded_(false),
|
||||
delete_files_(false) {
|
||||
|
||||
qLog(Debug) << "Starting";
|
||||
@@ -416,7 +430,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
ui_->tabs->AddTab(smartplaylists_view_, u"smartplaylists"_s, IconLoader::Load(u"view-media-playlist"_s, true, 0, 32), tr("Smart playlists"));
|
||||
ui_->tabs->AddTab(file_view_, u"files"_s, IconLoader::Load(u"document-open"_s, true, 0, 32), tr("Files"));
|
||||
ui_->tabs->AddTab(radio_view_, u"radios"_s, IconLoader::Load(u"radio"_s, true, 0, 32), tr("Radios"));
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
ui_->tabs->AddTab(device_view_, u"devices"_s, IconLoader::Load(u"device"_s, true, 0, 32), tr("Devices"));
|
||||
#endif
|
||||
#ifdef HAVE_SUBSONIC
|
||||
@@ -466,7 +480,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
|
||||
collection_view_->view()->setModel(app_->collection()->model()->filter());
|
||||
collection_view_->view()->Init(app->task_manager(), app->tagreader_client(), app->network(), app->albumcover_loader(), app->current_albumcover_loader(), app->cover_providers(), app->lyrics_providers(), app->collection(), app->device_manager(), app->streaming_services());
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
device_view_->view()->Init(app->task_manager(), app->tagreader_client(), app->device_manager(), app->collection_model()->directory_model());
|
||||
#endif
|
||||
playlist_list_->Init(app_->task_manager(), app->tagreader_client(), app_->playlist_manager(), app_->playlist_backend(), app_->device_manager());
|
||||
@@ -540,7 +554,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
QObject::connect(file_view_, &FileView::CopyToCollection, this, &MainWindow::CopyFilesToCollection);
|
||||
QObject::connect(file_view_, &FileView::MoveToCollection, this, &MainWindow::MoveFilesToCollection);
|
||||
QObject::connect(file_view_, &FileView::EditTags, this, &MainWindow::EditFileTags);
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
QObject::connect(file_view_, &FileView::CopyToDevice, this, &MainWindow::CopyFilesToDevice);
|
||||
#endif
|
||||
file_view_->SetTaskManager(app_->task_manager());
|
||||
@@ -654,7 +668,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
QObject::connect(&*app_->player(), &Player::Playing, playlist_list_, &PlaylistListContainer::ActivePlaying);
|
||||
QObject::connect(&*app_->player(), &Player::Stopped, playlist_list_, &PlaylistListContainer::ActiveStopped);
|
||||
|
||||
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::AllPlaylistsLoaded, &*app->player(), &Player::PlaylistsLoaded);
|
||||
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::AllPlaylistsLoaded, this, &MainWindow::PlaylistsLoaded);
|
||||
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, this, &MainWindow::SongChanged);
|
||||
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, &*app_->player(), &Player::CurrentMetadataChanged);
|
||||
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::EditingFinished, this, &MainWindow::PlaylistEditFinished);
|
||||
@@ -704,7 +718,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::triggered, this, &MainWindow::SearchCoverAutomatically);
|
||||
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::toggled, this, &MainWindow::ToggleSearchCoverAuto);
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
// Devices connections
|
||||
QObject::connect(device_view_->view(), &DeviceView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
|
||||
#endif
|
||||
@@ -810,7 +824,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
playlist_organize_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &MainWindow::PlaylistMoveToCollection);
|
||||
playlist_copy_to_collection_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &MainWindow::PlaylistCopyToCollection);
|
||||
playlist_move_to_collection_ = playlist_menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &MainWindow::PlaylistMoveToCollection);
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
playlist_copy_to_device_ = playlist_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &MainWindow::PlaylistCopyToDevice);
|
||||
#endif
|
||||
playlist_delete_ = playlist_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &MainWindow::PlaylistDelete);
|
||||
@@ -854,7 +868,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
|
||||
|
||||
// Windows 7 thumbbar buttons
|
||||
#ifdef Q_OS_WIN
|
||||
#ifdef Q_OS_WIN32
|
||||
thumbbar_->SetActions(QList<QAction*>() << ui_->action_previous_track << ui_->action_play_pause << ui_->action_stop << ui_->action_next_track << nullptr << ui_->action_love);
|
||||
#endif
|
||||
|
||||
@@ -966,7 +980,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdateLastPlayed, &*app_->collection_backend(), &CollectionBackend::UpdateLastPlayed);
|
||||
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdatePlayCount, &*app_->collection_backend(), &CollectionBackend::UpdatePlayCount);
|
||||
|
||||
#if !defined(HAVE_AUDIOCD) || defined(Q_OS_WIN)
|
||||
#if !defined(HAVE_AUDIOCD) || defined(Q_OS_WIN32)
|
||||
ui_->action_open_cd->setEnabled(false);
|
||||
ui_->action_open_cd->setVisible(false);
|
||||
#endif
|
||||
@@ -1317,6 +1331,9 @@ void MainWindow::ReloadAllSettings() {
|
||||
qobuz_view_->ReloadSettings();
|
||||
qobuz_view_->search_view()->ReloadSettings();
|
||||
#endif
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord_rich_presence_->ReloadSettings();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@@ -1392,6 +1409,19 @@ void MainWindow::ExitFinished() {
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::PlaylistsLoaded() {
|
||||
|
||||
playlists_loaded_ = true;
|
||||
|
||||
if (options_.has_value()) {
|
||||
CommandlineOptionsReceived(options_.value());
|
||||
options_.reset();
|
||||
}
|
||||
|
||||
app_->player()->PlaylistsLoaded();
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::MediaStopped() {
|
||||
|
||||
setWindowTitle(u"Strawberry Music Player"_s);
|
||||
@@ -1516,7 +1546,7 @@ void MainWindow::SongChanged(const Song &song) {
|
||||
|
||||
SendNowPlaying();
|
||||
|
||||
const bool enable_change_art = song.is_collection_song() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty();
|
||||
const bool enable_change_art = song.is_local_collection_song() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty();
|
||||
album_cover_choice_controller_->show_cover_action()->setEnabled(song.has_valid_art() && !song.art_unset());
|
||||
album_cover_choice_controller_->cover_to_file_action()->setEnabled(song.has_valid_art() && !song.art_unset());
|
||||
album_cover_choice_controller_->cover_from_file_action()->setEnabled(enable_change_art);
|
||||
@@ -1666,6 +1696,11 @@ void MainWindow::StopAfterCurrent() {
|
||||
|
||||
void MainWindow::showEvent(QShowEvent *e) {
|
||||
|
||||
if (error_dialog_ && error_dialog_->isVisible() && error_dialog_->isMinimized()) {
|
||||
error_dialog_->raise();
|
||||
error_dialog_->activateWindow();
|
||||
}
|
||||
|
||||
QMainWindow::showEvent(e);
|
||||
|
||||
}
|
||||
@@ -1695,7 +1730,12 @@ void MainWindow::closeEvent(QCloseEvent *e) {
|
||||
void MainWindow::SetHiddenInTray(const bool hidden) {
|
||||
|
||||
if (hidden && isVisible()) {
|
||||
close();
|
||||
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible() && keep_running_) {
|
||||
close();
|
||||
}
|
||||
else {
|
||||
showMinimized();
|
||||
}
|
||||
}
|
||||
else if (!hidden && isHidden()) {
|
||||
if (was_minimized_) {
|
||||
@@ -1992,7 +2032,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
||||
playlist_show_in_collection_->setVisible(false);
|
||||
playlist_copy_to_collection_->setVisible(false);
|
||||
playlist_move_to_collection_->setVisible(false);
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
playlist_copy_to_device_->setVisible(false);
|
||||
#endif
|
||||
playlist_organize_->setVisible(false);
|
||||
@@ -2067,7 +2107,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
||||
playlist_move_to_collection_->setVisible(local_songs > 0);
|
||||
}
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
playlist_copy_to_device_->setVisible(local_songs > 0);
|
||||
#endif
|
||||
|
||||
@@ -2437,6 +2477,11 @@ void MainWindow::CommandlineOptionsReceived(const QByteArray &string_options) {
|
||||
|
||||
void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
|
||||
|
||||
if (!playlists_loaded_) {
|
||||
options_ = options;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (options.player_action()) {
|
||||
case CommandlineOptions::PlayerAction::Play:
|
||||
if (options.urls().empty()) {
|
||||
@@ -2556,10 +2601,10 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
|
||||
}
|
||||
|
||||
if (options.seek_to() != -1) {
|
||||
app_->player()->SeekTo(options.seek_to());
|
||||
app_->player()->SeekTo(static_cast<quint64>(options.seek_to()));
|
||||
}
|
||||
else if (options.seek_by() != 0) {
|
||||
app_->player()->SeekTo(app_->player()->engine()->position_nanosec() / kNsecPerSec + options.seek_by());
|
||||
app_->player()->SeekTo(static_cast<quint64>(app_->player()->engine()->position_nanosec() / kNsecPerSec + options.seek_by()));
|
||||
}
|
||||
|
||||
if (options.play_track_at() != -1) app_->player()->PlayAt(options.play_track_at(), false, 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Maybe, true);
|
||||
@@ -2706,7 +2751,7 @@ void MainWindow::MoveFilesToCollection(const QList<QUrl> &urls) {
|
||||
|
||||
void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
|
||||
organize_dialog_->SetCopy(true);
|
||||
if (organize_dialog_->SetUrls(urls)) {
|
||||
@@ -2846,7 +2891,7 @@ void MainWindow::PlaylistSkip() {
|
||||
|
||||
void MainWindow::PlaylistCopyToDevice() {
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
|
||||
SongList songs;
|
||||
|
||||
@@ -2936,7 +2981,7 @@ void MainWindow::OpenSettingsDialog() {
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::OpenSettingsDialogAtPage(SettingsDialog::Page page) {
|
||||
void MainWindow::OpenSettingsDialogAtPage(const SettingsDialog::Page page) {
|
||||
settings_dialog_->OpenAtPage(page);
|
||||
}
|
||||
|
||||
@@ -3022,7 +3067,7 @@ void MainWindow::Raise() {
|
||||
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#ifdef Q_OS_WIN32
|
||||
bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr *result) {
|
||||
|
||||
if (exit_count_ == 0 && message) {
|
||||
@@ -3031,7 +3076,7 @@ bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr
|
||||
}
|
||||
return QMainWindow::nativeEvent(eventType, message, result);
|
||||
}
|
||||
#endif // Q_OS_WIN
|
||||
#endif // Q_OS_WIN32
|
||||
|
||||
void MainWindow::AutoCompleteTags() {
|
||||
|
||||
@@ -3187,7 +3232,7 @@ void MainWindow::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult
|
||||
|
||||
Q_EMIT AlbumCoverReady(song, result.album_cover.image);
|
||||
|
||||
const bool enable_change_art = song.is_collection_song() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty();
|
||||
const bool enable_change_art = song.is_local_collection_song() && !song.effective_albumartist().isEmpty() && !song.album().isEmpty();
|
||||
album_cover_choice_controller_->show_cover_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type::Unset);
|
||||
album_cover_choice_controller_->cover_to_file_action()->setEnabled(result.success && result.type != AlbumCoverLoaderResult::Type::Unset);
|
||||
album_cover_choice_controller_->cover_from_file_action()->setEnabled(enable_change_art);
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
@@ -52,6 +54,7 @@
|
||||
#include "core/platforminterface.h"
|
||||
#include "core/song.h"
|
||||
#include "core/settings.h"
|
||||
#include "core/commandlineoptions.h"
|
||||
#include "tagreader/tagreaderclient.h"
|
||||
#include "osd/osdbase.h"
|
||||
#include "playlist/playlist.h"
|
||||
@@ -70,7 +73,7 @@ class CollectionViewContainer;
|
||||
class CollectionFilter;
|
||||
class AlbumCoverChoiceController;
|
||||
class CommandlineOptions;
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
class DeviceViewContainer;
|
||||
#endif
|
||||
class EditTagDialog;
|
||||
@@ -92,18 +95,31 @@ class Ui_MainWindow;
|
||||
class StreamingSongsView;
|
||||
class StreamingTabsView;
|
||||
class SmartPlaylistsViewContainer;
|
||||
#ifdef Q_OS_WIN
|
||||
#ifdef Q_OS_WIN32
|
||||
class Windows7ThumbBar;
|
||||
#endif
|
||||
class AddStreamDialog;
|
||||
class LastFMImportDialog;
|
||||
class RadioViewContainer;
|
||||
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
namespace discord {
|
||||
class RichPresence;
|
||||
}
|
||||
#endif
|
||||
|
||||
class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd, const CommandlineOptions &options, QWidget *parent = nullptr);
|
||||
explicit MainWindow(Application *app,
|
||||
SharedPtr<SystemTrayIcon> tray_icon,
|
||||
OSDBase *osd,
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence,
|
||||
#endif
|
||||
const CommandlineOptions &options,
|
||||
QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
void SetHiddenInTray(const bool hidden);
|
||||
@@ -114,7 +130,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
void closeEvent(QCloseEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
#ifdef Q_OS_WIN
|
||||
#ifdef Q_OS_WIN32
|
||||
bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;
|
||||
#endif
|
||||
|
||||
@@ -131,6 +147,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
void AuthorizationUrlReceived(const QUrl &url);
|
||||
|
||||
private Q_SLOTS:
|
||||
void PlaylistsLoaded();
|
||||
|
||||
void FilePathChanged(const QString &path);
|
||||
|
||||
void MediaStopped();
|
||||
@@ -224,7 +242,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
SettingsDialog *CreateSettingsDialog();
|
||||
EditTagDialog *CreateEditTagDialog();
|
||||
void OpenSettingsDialog();
|
||||
void OpenSettingsDialogAtPage(SettingsDialog::Page page);
|
||||
void OpenSettingsDialogAtPage(const SettingsDialog::Page page);
|
||||
|
||||
void TabSwitched();
|
||||
void ToggleSidebar(const bool checked);
|
||||
@@ -289,13 +307,16 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
|
||||
private:
|
||||
Ui_MainWindow *ui_;
|
||||
#ifdef Q_OS_WIN
|
||||
#ifdef Q_OS_WIN32
|
||||
Windows7ThumbBar *thumbbar_;
|
||||
#endif
|
||||
|
||||
Application *app_;
|
||||
SharedPtr<SystemTrayIcon> tray_icon_;
|
||||
OSDBase *osd_;
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence_;
|
||||
#endif
|
||||
Lazy<About> about_dialog_;
|
||||
Lazy<Console> console_;
|
||||
Lazy<EditTagDialog> edit_tag_dialog_;
|
||||
@@ -306,7 +327,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
ContextView *context_view_;
|
||||
CollectionViewContainer *collection_view_;
|
||||
FileView *file_view_;
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
DeviceViewContainer *device_view_;
|
||||
#endif
|
||||
PlaylistListContainer *playlist_list_;
|
||||
@@ -359,7 +380,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
QAction *playlist_move_to_collection_;
|
||||
QAction *playlist_open_in_browser_;
|
||||
QAction *playlist_organize_;
|
||||
#ifndef Q_OS_WIN
|
||||
#ifndef Q_OS_WIN32
|
||||
QAction *playlist_copy_to_device_;
|
||||
#endif
|
||||
QAction *playlist_delete_;
|
||||
@@ -396,7 +417,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
AlbumCoverImageResult album_cover_;
|
||||
bool exit_;
|
||||
int exit_count_;
|
||||
bool playlists_loaded_;
|
||||
bool delete_files_;
|
||||
std::optional<CommandlineOptions> options_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -88,6 +88,4 @@ int MultiSortFilterProxy::Compare(const QVariant &left, const QVariant &right) c
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
@@ -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).toInt();
|
||||
port_ = s.value("port", 8080).toULongLong();
|
||||
use_authentication_ = s.value("use_authentication", false).toBool();
|
||||
username_ = s.value("username").toString();
|
||||
password_ = s.value("password").toString();
|
||||
|
||||
@@ -57,7 +57,7 @@ class NetworkProxyFactory : public QNetworkProxyFactory {
|
||||
Mode mode_;
|
||||
QNetworkProxy::ProxyType type_;
|
||||
QString hostname_;
|
||||
int port_;
|
||||
quint64 port_;
|
||||
bool use_authentication_;
|
||||
QString username_;
|
||||
QString password_;
|
||||
|
||||
607
src/core/oauthenticator.cpp
Normal file
607
src/core/oauthenticator.cpp
Normal file
@@ -0,0 +1,607 @@
|
||||
/*
|
||||
* 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 ¶m : 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 ¶ms, const bool refresh_token) {
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : 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);
|
||||
|
||||
}
|
||||
127
src/core/oauthenticator.h
Normal file
127
src/core/oauthenticator.h
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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 ¶ms, 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
|
||||
@@ -173,7 +173,7 @@ void Player::LoadVolume() {
|
||||
|
||||
Settings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
const uint volume = s.value(kVolume, 100).toInt();
|
||||
const uint volume = s.value(kVolume, 100).toUInt();
|
||||
s.endGroup();
|
||||
|
||||
SetVolume(volume);
|
||||
@@ -240,7 +240,7 @@ void Player::ResumePlayback() {
|
||||
s.beginGroup(kSettingsGroup);
|
||||
const EngineBase::State playback_state = static_cast<EngineBase::State>(s.value(kPlaybackState, static_cast<int>(EngineBase::State::Empty)).toInt());
|
||||
const int playback_playlist = s.value(kPlaybackPlaylist, -1).toInt();
|
||||
const int playback_position = s.value(kPlaybackPosition, 0).toInt();
|
||||
const quint64 playback_position = s.value(kPlaybackPosition, 0).toULongLong();
|
||||
s.endGroup();
|
||||
|
||||
if (playback_playlist == playlist_manager_->current()->id()) {
|
||||
@@ -372,7 +372,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
|
||||
|
||||
if (is_current) {
|
||||
qLog(Debug) << "Playing song" << current_item->Metadata().title() << result.stream_url_ << "position" << play_offset_nanosec_;
|
||||
engine_->Play(result.media_url_, result.stream_url_, pause_, stream_change_type_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec(), play_offset_nanosec_, song.ebur128_integrated_loudness_lufs());
|
||||
engine_->Play(result.media_url_, result.stream_url_, pause_, stream_change_type_, song.has_cue(), static_cast<quint64>(song.beginning_nanosec()), song.end_nanosec(), play_offset_nanosec_, song.ebur128_integrated_loudness_lufs());
|
||||
current_item_ = current_item;
|
||||
play_offset_nanosec_ = 0;
|
||||
}
|
||||
@@ -528,7 +528,7 @@ void Player::PlayPause(const quint64 offset_nanosec, const Playlist::AutoScroll
|
||||
}
|
||||
else {
|
||||
pause_time_ = QDateTime::currentDateTime();
|
||||
play_offset_nanosec_ = engine_->position_nanosec();
|
||||
play_offset_nanosec_ = static_cast<quint64>(engine_->position_nanosec());
|
||||
engine_->Pause();
|
||||
}
|
||||
break;
|
||||
@@ -556,10 +556,10 @@ void Player::UnPause() {
|
||||
if (current_item_ && pause_time_.isValid()) {
|
||||
const Song &song = current_item_->Metadata();
|
||||
if (url_handlers_->CanHandle(song.url()) && song.stream_url_can_expire()) {
|
||||
const quint64 time = QDateTime::currentSecsSinceEpoch() - pause_time_.toSecsSinceEpoch();
|
||||
const qint64 time = QDateTime::currentSecsSinceEpoch() - pause_time_.toSecsSinceEpoch();
|
||||
if (time >= 30) { // Stream URL might be expired.
|
||||
qLog(Debug) << "Re-requesting stream URL for" << song.url();
|
||||
play_offset_nanosec_ = engine_->position_nanosec();
|
||||
play_offset_nanosec_ = static_cast<quint64>(engine_->position_nanosec());
|
||||
UrlHandler *url_handler = url_handlers_->GetUrlHandler(song.url());
|
||||
HandleLoadResult(url_handler->StartLoading(song.url()));
|
||||
return;
|
||||
@@ -654,7 +654,7 @@ void Player::EngineStateChanged(const EngineBase::State state) {
|
||||
switch (state) {
|
||||
case EngineBase::State::Paused:
|
||||
pause_time_ = QDateTime::currentDateTime();
|
||||
play_offset_nanosec_ = engine_->position_nanosec();
|
||||
play_offset_nanosec_ = static_cast<quint64>(engine_->position_nanosec());
|
||||
Q_EMIT Paused();
|
||||
break;
|
||||
case EngineBase::State::Playing:
|
||||
@@ -774,7 +774,7 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
|
||||
}
|
||||
else {
|
||||
qLog(Debug) << "Playing song" << current_item_->Metadata().title() << url << "position" << offset_nanosec;
|
||||
engine_->Play(current_item_->Url(), url, pause, change, current_item_->Metadata().has_cue(), current_item_->effective_beginning_nanosec(), current_item_->effective_end_nanosec(), offset_nanosec, current_item_->effective_ebur128_integrated_loudness_lufs());
|
||||
engine_->Play(current_item_->Url(), url, pause, change, current_item_->Metadata().has_cue(), static_cast<quint64>(current_item_->effective_beginning_nanosec()), current_item_->effective_end_nanosec(), offset_nanosec, current_item_->effective_ebur128_integrated_loudness_lufs());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -782,7 +782,7 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
|
||||
void Player::CurrentMetadataChanged(const Song &metadata) {
|
||||
|
||||
// Those things might have changed (especially when a previously invalid song was reloaded) so we push the latest version into Engine
|
||||
engine_->RefreshMarkers(metadata.beginning_nanosec(), metadata.end_nanosec());
|
||||
engine_->RefreshMarkers(static_cast<quint64>(metadata.beginning_nanosec()), metadata.end_nanosec());
|
||||
|
||||
}
|
||||
|
||||
@@ -796,7 +796,7 @@ void Player::SeekTo(const quint64 seconds) {
|
||||
}
|
||||
|
||||
const qint64 nanosec = qBound(0LL, static_cast<qint64>(seconds) * kNsecPerSec, length_nanosec);
|
||||
engine_->Seek(nanosec);
|
||||
engine_->Seek(static_cast<quint64>(nanosec));
|
||||
|
||||
qLog(Debug) << "Track seeked to" << nanosec << "ns - updating scrobble point";
|
||||
playlist_manager_->active()->UpdateScrobblePoint(nanosec);
|
||||
@@ -810,11 +810,11 @@ void Player::SeekTo(const quint64 seconds) {
|
||||
}
|
||||
|
||||
void Player::SeekForward() {
|
||||
SeekTo(engine()->position_nanosec() / kNsecPerSec + seek_step_sec_);
|
||||
SeekTo(static_cast<quint64>(engine()->position_nanosec() / kNsecPerSec + seek_step_sec_));
|
||||
}
|
||||
|
||||
void Player::SeekBackward() {
|
||||
SeekTo(engine()->position_nanosec() / kNsecPerSec - seek_step_sec_);
|
||||
SeekTo(static_cast<quint64>(engine()->position_nanosec() / kNsecPerSec - seek_step_sec_));
|
||||
}
|
||||
|
||||
void Player::EngineMetadataReceived(const EngineMetadata &engine_metadata) {
|
||||
|
||||
@@ -439,6 +439,7 @@ 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_; }
|
||||
@@ -661,7 +662,8 @@ 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_collection_song() const { return d->source_ == Source::Collection; }
|
||||
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_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; }
|
||||
@@ -713,7 +715,8 @@ 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::WAV ||
|
||||
d->filetype_ == FileType::AIFF;
|
||||
|
||||
}
|
||||
|
||||
@@ -736,7 +739,8 @@ bool Song::performer_supported() const {
|
||||
d->filetype_ == FileType::MPEG ||
|
||||
d->filetype_ == FileType::MPC ||
|
||||
d->filetype_ == FileType::APE ||
|
||||
d->filetype_ == FileType::WAV;
|
||||
d->filetype_ == FileType::WAV ||
|
||||
d->filetype_ == FileType::AIFF;
|
||||
|
||||
}
|
||||
|
||||
@@ -764,7 +768,9 @@ bool Song::rating_supported() const {
|
||||
d->filetype_ == FileType::MP4 ||
|
||||
d->filetype_ == FileType::ASF ||
|
||||
d->filetype_ == FileType::MPC ||
|
||||
d->filetype_ == FileType::APE;
|
||||
d->filetype_ == FileType::APE ||
|
||||
d->filetype_ == FileType::WAV ||
|
||||
d->filetype_ == FileType::AIFF;
|
||||
|
||||
}
|
||||
|
||||
@@ -782,7 +788,9 @@ bool Song::save_embedded_cover_supported(const FileType filetype) {
|
||||
filetype == FileType::OggVorbis ||
|
||||
filetype == FileType::OggOpus ||
|
||||
filetype == FileType::MPEG ||
|
||||
filetype == FileType::MP4;
|
||||
filetype == FileType::MP4 ||
|
||||
filetype == FileType::WAV ||
|
||||
filetype == FileType::AIFF;
|
||||
|
||||
}
|
||||
|
||||
@@ -1027,8 +1035,10 @@ 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;
|
||||
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;
|
||||
}
|
||||
|
||||
Song::Source Song::SourceFromURL(const QUrl &url) {
|
||||
@@ -1331,6 +1341,12 @@ 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) {
|
||||
@@ -1831,8 +1847,8 @@ bool Song::MergeFromEngineMetadata(const EngineMetadata &engine_metadata) {
|
||||
|
||||
bool minor = true;
|
||||
|
||||
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 (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 (title() != engine_metadata.title && title().isEmpty() && !engine_metadata.title.isEmpty()) {
|
||||
set_title(engine_metadata.title);
|
||||
minor = false;
|
||||
@@ -1909,7 +1925,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());
|
||||
return qHash(song.title().toLower()) ^ qHash(song.artist().toLower()) ^ qHash(song.album().toLower()) ^ qHash(song.fingerprint()) ^ qHash(song.acoustid_fingerprint());
|
||||
}
|
||||
|
||||
bool Song::ContainsRegexList(const QString &str, const RegularExpressionList ®ex_list) {
|
||||
|
||||
@@ -78,6 +78,7 @@ class Song {
|
||||
RadioParadise = 10,
|
||||
Spotify = 11
|
||||
};
|
||||
static const int kSourceCount = 16;
|
||||
|
||||
enum class FileType {
|
||||
Unknown = 0,
|
||||
@@ -176,6 +177,7 @@ class Song {
|
||||
int bitdepth() const;
|
||||
|
||||
Source source() const;
|
||||
int source_id() const;
|
||||
int directory_id() const;
|
||||
const QUrl &url() const;
|
||||
const QString &basefilename() const;
|
||||
@@ -372,7 +374,8 @@ class Song {
|
||||
const QString &playlist_albumartist_sortable() const;
|
||||
|
||||
bool is_metadata_good() const;
|
||||
bool is_collection_song() const;
|
||||
bool is_local_collection_song() const;
|
||||
bool is_linked_collection_song() const;
|
||||
bool is_stream() const;
|
||||
bool is_radio() const;
|
||||
bool is_cdda() const;
|
||||
@@ -459,6 +462,7 @@ 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
|
||||
|
||||
@@ -55,7 +55,7 @@ void StandardItemIconLoader::SetModel(QAbstractItemModel *model) {
|
||||
|
||||
}
|
||||
|
||||
void StandardItemIconLoader::LoadIcon(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item) {
|
||||
void StandardItemIconLoader::LoadAlbumCover(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::LoadIcon(const QUrl &art_automatic, const QUrl &art
|
||||
|
||||
}
|
||||
|
||||
void StandardItemIconLoader::LoadIcon(const Song &song, QStandardItem *for_item) {
|
||||
void StandardItemIconLoader::LoadAlbumCover(const Song &song, QStandardItem *for_item) {
|
||||
|
||||
AlbumCoverLoaderOptions cover_options(AlbumCoverLoaderOptions::Option::ScaledImage);
|
||||
cover_options.desired_scaled_size = QSize(16, 16);
|
||||
|
||||
@@ -46,8 +46,8 @@ class StandardItemIconLoader : public QObject {
|
||||
|
||||
void SetModel(QAbstractItemModel *model);
|
||||
|
||||
void LoadIcon(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item);
|
||||
void LoadIcon(const Song &song, QStandardItem *for_item);
|
||||
void LoadAlbumCover(const QUrl &art_automatic, const QUrl &art_manual, QStandardItem *for_item);
|
||||
void LoadAlbumCover(const Song &song, QStandardItem *for_item);
|
||||
|
||||
private Q_SLOTS:
|
||||
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
|
||||
|
||||
@@ -28,21 +28,29 @@
|
||||
|
||||
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 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 QString &error) : media_url_(media_url),
|
||||
type_(type),
|
||||
filetype_(Song::FileType::Stream),
|
||||
samplerate_(-1),
|
||||
bit_depth_(-1),
|
||||
length_nanosec_(-1),
|
||||
error_(error)
|
||||
{}
|
||||
|
||||
@@ -56,7 +56,6 @@ 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().
|
||||
|
||||
@@ -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 = i;
|
||||
button->iId = static_cast<UINT>(i);
|
||||
SetupButton(action, button);
|
||||
}
|
||||
|
||||
qLog(Debug) << "Adding" << actions_.count() << "buttons";
|
||||
HRESULT hr = taskbar_list_->ThumbBarAddButtons(reinterpret_cast<HWND>(widget_->winId()), actions_.count(), buttons);
|
||||
HRESULT hr = taskbar_list_->ThumbBarAddButtons(reinterpret_cast<HWND>(widget_->winId()), static_cast<UINT>(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 = i;
|
||||
button->iId = static_cast<UINT>(i);
|
||||
SetupButton(action, button);
|
||||
}
|
||||
|
||||
HRESULT hr = taskbar_list_->ThumbBarUpdateButtons(reinterpret_cast<HWND>(widget_->winId()), actions_.count(), buttons);
|
||||
HRESULT hr = taskbar_list_->ThumbBarUpdateButtons(reinterpret_cast<HWND>(widget_->winId()), static_cast<UINT>(actions_.count()), buttons);
|
||||
if (hr != S_OK) {
|
||||
qLog(Debug) << "Failed to update buttons" << Qt::hex << DWORD(hr);
|
||||
}
|
||||
|
||||
@@ -98,10 +98,10 @@ void AlbumCoverFetcherSearch::Start(SharedPtr<CoverProviders> cover_providers) {
|
||||
|
||||
for (CoverProvider *provider : std::as_const(cover_providers_sorted)) {
|
||||
|
||||
if (!provider->is_enabled()) continue;
|
||||
if (!provider->enabled()) continue;
|
||||
|
||||
// Skip any provider that requires authentication but is not authenticated.
|
||||
if (provider->AuthenticationRequired() && !provider->IsAuthenticated()) {
|
||||
if (provider->authentication_required() && !provider->authenticated()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -249,6 +249,8 @@ void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverPr
|
||||
|
||||
void AlbumCoverFetcherSearch::AllProvidersFinished() {
|
||||
|
||||
qLog(Debug) << "Search finished, got" << results_.count() << "results";
|
||||
|
||||
if (cancel_requested_) {
|
||||
return;
|
||||
}
|
||||
@@ -285,9 +287,9 @@ void AlbumCoverFetcherSearch::FetchMoreImages() {
|
||||
|
||||
qLog(Debug) << "Loading" << result.artist << result.album << result.image_url << "from" << result.provider << "with current score" << result.score();
|
||||
|
||||
QNetworkRequest req(result.image_url);
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
QNetworkReply *image_reply = network_->get(req);
|
||||
QNetworkRequest network_request(result.image_url);
|
||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
QNetworkReply *image_reply = network_->get(network_request);
|
||||
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);
|
||||
|
||||
@@ -397,9 +397,9 @@ AlbumCoverLoader::LoadImageResult AlbumCoverLoader::LoadRemoteUrlImage(TaskPtr t
|
||||
|
||||
qLog(Debug) << "Loading remote cover from URL" << cover_url;
|
||||
|
||||
QNetworkRequest request(cover_url);
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
QNetworkReply *reply = network_->get(request);
|
||||
QNetworkRequest network_request(cover_url);
|
||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
QNetworkReply *reply = network_->get(network_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 request = reply->request();
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setUrl(redirect_url);
|
||||
QNetworkReply *redirected_reply = network_->get(request);
|
||||
QNetworkRequest network_request = reply->request();
|
||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
network_request.setUrl(redirect_url);
|
||||
QNetworkReply *redirected_reply = network_->get(network_request);
|
||||
QObject::connect(redirected_reply, &QNetworkReply::finished, this, [this, reply, task, result_type, redirect_url]() { LoadRemoteImageFinished(reply, task, result_type, redirect_url); });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,10 +73,10 @@ void CoverFromURLDialog::accept() {
|
||||
|
||||
ui_->busy->show();
|
||||
|
||||
QNetworkRequest req(QUrl::fromUserInput(ui_->url->text()));
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
QNetworkRequest network_request(QUrl::fromUserInput(ui_->url->text()));
|
||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
QNetworkReply *reply = network_->get(network_request);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, &CoverFromURLDialog::LoadCoverFromURLFinished);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -21,10 +21,9 @@
|
||||
|
||||
#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) : QObject(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) : 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) {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -24,28 +24,24 @@
|
||||
|
||||
#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;
|
||||
|
||||
// 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 {
|
||||
class CoverProvider : public JsonBaseRequest {
|
||||
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_; }
|
||||
bool enabled() const { return enabled_; }
|
||||
int order() const { return order_; }
|
||||
float quality() const { return quality_; }
|
||||
bool batch() const { return batch_; }
|
||||
@@ -54,10 +50,14 @@ class CoverProvider : public QObject {
|
||||
void set_enabled(const bool enabled) { enabled_ = enabled; }
|
||||
void set_order(const int order) { order_ = order; }
|
||||
|
||||
bool AuthenticationRequired() const { return authentication_required_; }
|
||||
virtual bool IsAuthenticated() const { return true; }
|
||||
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(); }
|
||||
|
||||
virtual void Authenticate() {}
|
||||
virtual void Deauthenticate() {}
|
||||
virtual void ClearSession() {}
|
||||
|
||||
// Starts searching for covers matching the given query text.
|
||||
// Returns true if the query has been started, or false if an error occurred.
|
||||
@@ -65,12 +65,10 @@ class CoverProvider : public QObject {
|
||||
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 AuthenticationComplete(const bool success, const QStringList &errors = QStringList());
|
||||
void AuthenticationFinished(const bool success, const QString &error = QString());
|
||||
void AuthenticationSuccess();
|
||||
void AuthenticationFailure(const QStringList &errors);
|
||||
void AuthenticationFailure(const QString &error);
|
||||
void SearchResults(const int id, const CoverProviderSearchResults &results);
|
||||
void SearchFinished(const int id, const CoverProviderSearchResults &results);
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ void CoverProviders::ReloadSettings() {
|
||||
QMap<int, QString> all_providers;
|
||||
QList<CoverProvider*> old_providers = cover_providers_.keys();
|
||||
for (CoverProvider *provider : std::as_const(old_providers)) {
|
||||
if (!provider->is_enabled()) continue;
|
||||
if (!provider->enabled()) continue;
|
||||
all_providers.insert(provider->order(), provider->name());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -22,8 +22,6 @@
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QPair>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
@@ -38,6 +36,7 @@
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/logging.h"
|
||||
@@ -52,22 +51,11 @@ 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;
|
||||
@@ -91,17 +79,7 @@ bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &albu
|
||||
<< Param(u"q"_s, query)
|
||||
<< Param(u"limit"_s, QString::number(kLimit));
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : 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;
|
||||
QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kApiUrl) + QLatin1Char('/') + resource), params);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); });
|
||||
|
||||
return true;
|
||||
@@ -110,82 +88,56 @@ bool DeezerCoverProvider::StartSearch(const QString &artist, const QString &albu
|
||||
|
||||
void DeezerCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); }
|
||||
|
||||
QByteArray DeezerCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
JsonBaseRequest::JsonObjectResult DeezerCoverProvider::ParseJsonObject(QNetworkReply *reply) {
|
||||
|
||||
QByteArray data;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
data = reply->readAll();
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
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);
|
||||
|
||||
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 {
|
||||
// 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);
|
||||
result.error_code = ErrorCode::ParseError;
|
||||
result.error_message = json_parse_error.errorString();
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
@@ -196,78 +148,90 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
Q_EMIT SearchFinished(id, CoverProviderSearchResults());
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_data = ExtractData(data);
|
||||
if (!value_data.isArray()) {
|
||||
Q_EMIT SearchFinished(id, CoverProviderSearchResults());
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array_data = value_data.toArray();
|
||||
if (array_data.isEmpty()) {
|
||||
Q_EMIT SearchFinished(id, CoverProviderSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
QMap<QUrl, CoverProviderSearchResult> results;
|
||||
QMap<QUrl, CoverProviderSearchResult> cover_results;
|
||||
int i = 0;
|
||||
for (const QJsonValue &json_value : std::as_const(array_data)) {
|
||||
for (const QJsonValue &value_entry : std::as_const(array_data)) {
|
||||
|
||||
if (!json_value.isObject()) {
|
||||
if (!value_entry.isObject()) {
|
||||
Error(u"Invalid Json reply, data array value is not a object."_s);
|
||||
continue;
|
||||
}
|
||||
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();
|
||||
const QJsonObject object_entry = value_entry.toObject();
|
||||
QJsonObject object_album;
|
||||
if (object_entry.contains("album"_L1) && object_entry["album"_L1].isObject()) { // Song search, so extract the album.
|
||||
object_album = object_entry["album"_L1].toObject();
|
||||
}
|
||||
else {
|
||||
obj_album = json_obj;
|
||||
object_album = object_entry;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!object_entry.contains("id"_L1) || !object_album.contains("id"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value object is missing ID."_s, object_entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!obj_album.contains("type"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value album object is missing type."_s, obj_album);
|
||||
if (!object_album.contains("type"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value album object is missing type."_s, object_album);
|
||||
continue;
|
||||
}
|
||||
QString type = obj_album["type"_L1].toString();
|
||||
const QString type = object_album["type"_L1].toString();
|
||||
if (type != "album"_L1) {
|
||||
Error(u"Invalid Json reply, data array value album object has incorrect type returned"_s, obj_album);
|
||||
Error(u"Invalid Json reply, data array value album object has incorrect type returned"_s, object_album);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("artist"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value object is missing artist."_s, json_obj);
|
||||
if (!object_entry.contains("artist"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value object is missing artist."_s, object_entry);
|
||||
continue;
|
||||
}
|
||||
QJsonValue value_artist = json_obj["artist"_L1];
|
||||
const QJsonValue value_artist = object_entry["artist"_L1];
|
||||
if (!value_artist.isObject()) {
|
||||
Error(u"Invalid Json reply, data array value artist is not a object."_s, value_artist);
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_artist = value_artist.toObject();
|
||||
const QJsonObject object_artist = value_artist.toObject();
|
||||
|
||||
if (!obj_artist.contains("name"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value artist object is missing name."_s, obj_artist);
|
||||
if (!object_artist.contains("name"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value artist object is missing name."_s, object_artist);
|
||||
continue;
|
||||
}
|
||||
QString artist = obj_artist["name"_L1].toString();
|
||||
const QString artist = object_artist["name"_L1].toString();
|
||||
|
||||
if (!obj_album.contains("title"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value album object is missing title."_s, obj_album);
|
||||
if (!object_album.contains("title"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value album object is missing title."_s, object_album);
|
||||
continue;
|
||||
}
|
||||
QString album = obj_album["title"_L1].toString();
|
||||
const QString album = object_album["title"_L1].toString();
|
||||
|
||||
CoverProviderSearchResult cover_result;
|
||||
cover_result.artist = artist;
|
||||
@@ -277,35 +241,29 @@ 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 (!obj_album.contains(cover_size.first)) continue;
|
||||
QString cover = obj_album[cover_size.first].toString();
|
||||
if (!object_album.contains(cover_size.first)) continue;
|
||||
const QString cover = object_album[cover_size.first].toString();
|
||||
if (!have_cover) {
|
||||
have_cover = true;
|
||||
++i;
|
||||
}
|
||||
QUrl url(cover);
|
||||
if (!results.contains(url)) {
|
||||
const QUrl url(cover);
|
||||
if (!cover_results.contains(url)) {
|
||||
cover_result.image_url = url;
|
||||
cover_result.image_size = cover_size.second;
|
||||
cover_result.number = i;
|
||||
results.insert(url, cover_result);
|
||||
cover_results.insert(url, cover_result);
|
||||
}
|
||||
}
|
||||
|
||||
if (!have_cover) {
|
||||
Error(u"Invalid Json reply, data array value album object is missing cover."_s, obj_album);
|
||||
Error(u"Invalid Json reply, data array value album object is missing cover."_s, object_album);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
results = cover_results.values();
|
||||
std::stable_sort(results.begin(), results.end(), AlbumCoverFetcherSearch::CoverProviderSearchResultCompareNumber);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -22,13 +22,8 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
@@ -40,7 +35,6 @@ 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;
|
||||
@@ -48,13 +42,11 @@ class DeezerCoverProvider : public JsonCoverProvider {
|
||||
private Q_SLOTS:
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id);
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonValue ExtractData(const QByteArray &data);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant()) override;
|
||||
protected:
|
||||
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
QList<QNetworkReply*> replies_;
|
||||
void Error(const QString &error, const QVariant &debug = QVariant()) override;
|
||||
};
|
||||
|
||||
#endif // DEEZERCOVERPROVIDER_H
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, Martin Björklund <mbj4668@gmail.com>
|
||||
* Copyright 2016-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2016-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
|
||||
@@ -24,8 +24,6 @@
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QPair>
|
||||
#include <QVariant>
|
||||
@@ -40,6 +38,7 @@
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonArray>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/logging.h"
|
||||
@@ -146,81 +145,88 @@ void DiscogsCoverProvider::SendSearchRequest(SharedPtr<DiscogsCoverSearchContext
|
||||
|
||||
}
|
||||
|
||||
QNetworkReply *DiscogsCoverProvider::CreateRequest(QUrl url, const ParamList ¶ms_provided) {
|
||||
QNetworkReply *DiscogsCoverProvider::CreateRequest(const QUrl &url, const ParamList ¶ms) {
|
||||
|
||||
const ParamList params = ParamList() << Param(u"key"_s, QString::fromLatin1(QByteArray::fromBase64(kAccessKeyB64)))
|
||||
<< Param(u"secret"_s, QString::fromLatin1(QByteArray::fromBase64(kSecretKeyB64)))
|
||||
<< 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;
|
||||
|
||||
QUrlQuery url_query;
|
||||
QStringList query_items;
|
||||
|
||||
// Encode the arguments
|
||||
using EncodedParam = QPair<QByteArray, QByteArray>;
|
||||
for (const Param ¶m : params) {
|
||||
EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
|
||||
for (const Param ¶m : request_params) {
|
||||
const 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));
|
||||
}
|
||||
url.setQuery(url_query);
|
||||
|
||||
QUrl request_url(url);
|
||||
request_url.setQuery(url_query);
|
||||
|
||||
// Sign the request
|
||||
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 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 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 req(url);
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
QNetworkRequest network_request(request_url);
|
||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
QNetworkReply *reply = network_->get(network_request);
|
||||
replies_ << reply;
|
||||
|
||||
qLog(Debug) << "Discogs: Sending request" << url;
|
||||
qLog(Debug) << "Discogs: Sending request" << request_url;
|
||||
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
QByteArray DiscogsCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
JsonBaseRequest::JsonObjectResult DiscogsCoverProvider::ParseJsonObject(QNetworkReply *reply) {
|
||||
|
||||
QByteArray data;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
data = reply->readAll();
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
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);
|
||||
|
||||
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 {
|
||||
// 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);
|
||||
result.error_code = ErrorCode::ParseError;
|
||||
result.error_message = json_parse_error.errorString();
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return data;
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@@ -234,37 +240,34 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
if (!requests_search_.contains(id)) return;
|
||||
SharedPtr<DiscogsCoverSearchContext> search = requests_search_.value(id);
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
EndSearch(search);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
EndSearch(search);
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
if (json_object.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_results;
|
||||
if (json_obj.contains("results"_L1)) {
|
||||
value_results = json_obj["results"_L1];
|
||||
if (json_object.contains("results"_L1)) {
|
||||
value_results = json_object["results"_L1];
|
||||
}
|
||||
else if (json_obj.contains("message"_L1)) {
|
||||
QString message = json_obj["message"_L1].toString();
|
||||
Error(QStringLiteral("%1").arg(message));
|
||||
EndSearch(search);
|
||||
else if (json_object.contains("message"_L1)) {
|
||||
Error(json_object["message"_L1].toString());
|
||||
return;
|
||||
}
|
||||
else {
|
||||
Error(u"Json object is missing results."_s, json_obj);
|
||||
EndSearch(search);
|
||||
Error(u"Json object is missing results."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value_results.isArray()) {
|
||||
Error(u"Missing results array."_s, value_results);
|
||||
EndSearch(search);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,19 +278,19 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
Error(u"Invalid Json reply, results value is not a object."_s);
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
quint64 release_id = obj_result["id"_L1].toInt();
|
||||
QUrl resource_url(obj_result["resource_url"_L1].toString());
|
||||
QString title = obj_result["title"_L1].toString();
|
||||
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();
|
||||
|
||||
if (title.contains(" - "_L1)) {
|
||||
QStringList title_splitted = title.split(u" - "_s);
|
||||
if (title_splitted.count() == 2) {
|
||||
QString artist = title_splitted.first();
|
||||
const 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;
|
||||
}
|
||||
@@ -300,14 +303,9 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
StartReleaseRequest(search, release_id, resource_url);
|
||||
}
|
||||
|
||||
if (search->requests_release_.count() == 0) {
|
||||
if (search->type == DiscogsCoverType::Master) {
|
||||
search->type = DiscogsCoverType::Release;
|
||||
queue_search_requests_.enqueue(search);
|
||||
}
|
||||
else {
|
||||
EndSearch(search);
|
||||
}
|
||||
if (search->requests_release_.count() == 0 && search->type == DiscogsCoverType::Master) {
|
||||
search->type = DiscogsCoverType::Release;
|
||||
queue_search_requests_.enqueue(search);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -344,33 +342,31 @@ 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);
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
EndSearch(search, 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);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
EndSearch(search, release.id);
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
if (json_object.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!json_object.contains("artists"_L1) || !json_object.contains("title"_L1)) {
|
||||
Error(u"Json reply object is missing artists or title."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("images"_L1)) {
|
||||
EndSearch(search, release.id);
|
||||
if (!json_object.contains("images"_L1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_artists = json_obj["artists"_L1];
|
||||
const QJsonValue value_artists = json_object["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();
|
||||
@@ -381,39 +377,35 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
|
||||
Error(u"Invalid Json reply, atists array value is not a object."_s);
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
artist = obj_artist["name"_L1].toString();
|
||||
artist = object_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;
|
||||
|
||||
QString album = json_obj["title"_L1].toString();
|
||||
const QString album = json_object["title"_L1].toString();
|
||||
if (artist != search->artist && album != search->album) {
|
||||
EndSearch(search, release.id);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_images = json_obj["images"_L1];
|
||||
const QJsonValue value_images = json_object["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;
|
||||
}
|
||||
|
||||
@@ -423,17 +415,17 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
|
||||
Error(u"Invalid Json reply, images array value is not an object."_s);
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_image = value_image.toObject();
|
||||
const 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;
|
||||
}
|
||||
QString type = obj_image["type"_L1].toString();
|
||||
const QString type = obj_image["type"_L1].toString();
|
||||
if (type != "primary"_L1) {
|
||||
continue;
|
||||
}
|
||||
int width = obj_image["width"_L1].toInt();
|
||||
int height = obj_image["height"_L1].toInt();
|
||||
const int width = obj_image["width"_L1].toInt();
|
||||
const 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;
|
||||
@@ -448,8 +440,6 @@ 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) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, Martin Björklund <mbj4668@gmail.com>
|
||||
* Copyright 2016-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2016-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
|
||||
@@ -24,16 +24,11 @@
|
||||
|
||||
#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"
|
||||
@@ -59,7 +54,7 @@ class DiscogsCoverProvider : public JsonCoverProvider {
|
||||
};
|
||||
|
||||
struct DiscogsCoverReleaseContext {
|
||||
explicit DiscogsCoverReleaseContext(const quint64 _search_id = 0, const quint64 _id = 0, const QUrl &_url = QUrl()) : search_id(_search_id), id(_id), url(_url) {}
|
||||
explicit DiscogsCoverReleaseContext(const int _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;
|
||||
@@ -77,9 +72,9 @@ class DiscogsCoverProvider : public JsonCoverProvider {
|
||||
private:
|
||||
void SendSearchRequest(SharedPtr<DiscogsCoverSearchContext> search);
|
||||
void SendReleaseRequest(const DiscogsCoverReleaseContext &release);
|
||||
QNetworkReply *CreateRequest(QUrl url, const ParamList ¶ms_provided = ParamList());
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QNetworkReply *CreateRequest(const QUrl &url, const ParamList ¶ms = ParamList());
|
||||
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;
|
||||
|
||||
@@ -93,7 +88,6 @@ 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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -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,32 +35,31 @@ 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::ExtractJsonObj(const QByteArray &data) {
|
||||
QJsonObject JsonCoverProvider::ExtractJsonObject(const QByteArray &data) {
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
const QJsonDocument json_document = 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_doc.isEmpty()) {
|
||||
if (json_document.isEmpty()) {
|
||||
Error(u"Received empty Json document."_s, data);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error(u"Json document is not an object."_s, json_doc);
|
||||
if (!json_document.isObject()) {
|
||||
Error(u"Json document is not an object."_s, json_document);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error(u"Received empty Json object."_s, json_doc);
|
||||
const QJsonObject json_object = json_document.object();
|
||||
if (json_object.isEmpty()) {
|
||||
Error(u"Received empty Json object."_s, json_document);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
return json_object;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
@@ -39,7 +38,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 ExtractJsonObj(const QByteArray &data);
|
||||
QJsonObject ExtractJsonObject(const QByteArray &data);
|
||||
};
|
||||
|
||||
#endif // JSONCOVERPROVIDER_H
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -22,10 +22,7 @@
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include <QObject>
|
||||
#include <QLocale>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
@@ -37,6 +34,7 @@
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
@@ -57,17 +55,6 @@ constexpr char kSecret[] = "80fd738f49596e9709b1bf9319c444a8";
|
||||
LastFmCoverProvider::LastFmCoverProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
||||
: JsonCoverProvider(u"Last.fm"_s, true, false, 1.0, true, false, network, parent) {}
|
||||
|
||||
LastFmCoverProvider::~LastFmCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
if (artist.isEmpty() && album.isEmpty() && title.isEmpty()) return false;
|
||||
@@ -111,18 +98,62 @@ bool LastFmCoverProvider::StartSearch(const QString &artist, const QString &albu
|
||||
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(u"api_sig"_s)), QString::fromLatin1(QUrl::toPercentEncoding(signature)));
|
||||
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(u"format"_s)), QString::fromLatin1(QUrl::toPercentEncoding(u"json"_s)));
|
||||
|
||||
QUrl url(QString::fromLatin1(kUrl));
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
|
||||
QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8());
|
||||
replies_ << reply;
|
||||
QNetworkReply *reply = CreatePostRequest(QUrl(QLatin1String(kUrl)), url_query);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, type]() { QueryFinished(reply, id, type); });
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
JsonBaseRequest::JsonObjectResult LastFmCoverProvider::ParseJsonObject(QNetworkReply *reply) {
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
|
||||
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.contains("message"_L1)) {
|
||||
const int error = json_object["error"_L1].toInt();
|
||||
const QString message = json_object["message"_L1].toString();
|
||||
result.error_code = ErrorCode::APIError;
|
||||
result.error_message = QStringLiteral("%1 (%2)").arg(message).arg(error);
|
||||
}
|
||||
else {
|
||||
result.json_object = json_document.object();
|
||||
}
|
||||
}
|
||||
else {
|
||||
result.error_code = ErrorCode::ParseError;
|
||||
result.error_message = json_parse_error.errorString();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, const QString &type) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
@@ -131,96 +162,85 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons
|
||||
reply->deleteLater();
|
||||
|
||||
CoverProviderSearchResults results;
|
||||
const QScopeGuard end_search = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); });
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
if (json_object.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_results;
|
||||
if (json_obj.contains("results"_L1)) {
|
||||
value_results = json_obj["results"_L1];
|
||||
if (json_object.contains("results"_L1)) {
|
||||
value_results = json_object["results"_L1];
|
||||
}
|
||||
else if (json_obj.contains("error"_L1) && json_obj.contains("message"_L1)) {
|
||||
int error = json_obj["error"_L1].toInt();
|
||||
QString message = json_obj["message"_L1].toString();
|
||||
else if (json_object.contains("error"_L1) && json_object.contains("message"_L1)) {
|
||||
const int error = json_object["error"_L1].toInt();
|
||||
const QString message = json_object["message"_L1].toString();
|
||||
Error(QStringLiteral("Error: %1: %2").arg(QString::number(error), message));
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
Error(u"Json reply is missing results."_s, json_obj);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
Error(u"Json reply is missing results."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value_results.isObject()) {
|
||||
Error(u"Json results is not a object."_s, value_results);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj_results = value_results.toObject();
|
||||
if (obj_results.isEmpty()) {
|
||||
const QJsonObject object_results = value_results.toObject();
|
||||
if (object_results.isEmpty()) {
|
||||
Error(u"Json results object is empty."_s, value_results);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_matches;
|
||||
|
||||
if (type == "album"_L1) {
|
||||
if (obj_results.contains("albummatches"_L1)) {
|
||||
value_matches = obj_results["albummatches"_L1];
|
||||
if (object_results.contains("albummatches"_L1)) {
|
||||
value_matches = object_results["albummatches"_L1];
|
||||
}
|
||||
else {
|
||||
Error(u"Json results object is missing albummatches."_s, obj_results);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
Error(u"Json results object is missing albummatches."_s, object_results);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (type == "track"_L1) {
|
||||
if (obj_results.contains("trackmatches"_L1)) {
|
||||
value_matches = obj_results["trackmatches"_L1];
|
||||
if (object_results.contains("trackmatches"_L1)) {
|
||||
value_matches = object_results["trackmatches"_L1];
|
||||
}
|
||||
else {
|
||||
Error(u"Json results object is missing trackmatches."_s, obj_results);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
Error(u"Json results object is missing trackmatches."_s, object_results);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!value_matches.isObject()) {
|
||||
Error(u"Json albummatches or trackmatches is not an object."_s, value_matches);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj_matches = value_matches.toObject();
|
||||
if (obj_matches.isEmpty()) {
|
||||
const QJsonObject object_matches = value_matches.toObject();
|
||||
if (object_matches.isEmpty()) {
|
||||
Error(u"Json albummatches or trackmatches object is empty."_s, value_matches);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonValue value_type;
|
||||
if (!obj_matches.contains(type)) {
|
||||
Error(QStringLiteral("Json object is missing %1.").arg(type), obj_matches);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
if (!object_matches.contains(type)) {
|
||||
Error(QStringLiteral("Json object is missing %1.").arg(type), object_matches);
|
||||
return;
|
||||
}
|
||||
value_type = obj_matches[type];
|
||||
const QJsonValue value_type = object_matches[type];
|
||||
|
||||
if (!value_type.isArray()) {
|
||||
Error(u"Json album value in albummatches object is not an array."_s, value_type);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
const QJsonArray array_type = value_type.toArray();
|
||||
@@ -231,23 +251,22 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons
|
||||
Error(u"Invalid Json reply, value in albummatches/trackmatches array is not a object."_s);
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj = value.toObject();
|
||||
if (!obj.contains("artist"_L1) || !obj.contains("image"_L1) || !obj.contains("name"_L1)) {
|
||||
Error(u"Invalid Json reply, album is missing artist, image or name."_s, obj);
|
||||
const QJsonObject object = value.toObject();
|
||||
if (!object.contains("artist"_L1) || !object.contains("image"_L1) || !object.contains("name"_L1)) {
|
||||
Error(u"Invalid Json reply, album is missing artist, image or name."_s, object);
|
||||
continue;
|
||||
}
|
||||
QString artist = obj["artist"_L1].toString();
|
||||
const QString artist = object["artist"_L1].toString();
|
||||
QString album;
|
||||
if (type == "album"_L1) {
|
||||
album = obj["name"_L1].toString();
|
||||
album = object["name"_L1].toString();
|
||||
}
|
||||
|
||||
QJsonValue json_image = obj["image"_L1];
|
||||
if (!json_image.isArray()) {
|
||||
Error(u"Invalid Json reply, album image is not a array."_s, json_image);
|
||||
if (!object.contains("image"_L1) || !object["image"_L1].isArray()) {
|
||||
Error(u"Invalid Json reply, album image is not a array."_s, object);
|
||||
continue;
|
||||
}
|
||||
const QJsonArray array_image = json_image.toArray();
|
||||
const QJsonArray array_image = object["image"_L1].toArray();
|
||||
QString image_url_use;
|
||||
LastFmImageSize image_size_use = LastFmImageSize::Unknown;
|
||||
for (const QJsonValue &value_image : array_image) {
|
||||
@@ -255,14 +274,14 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons
|
||||
Error(u"Invalid Json reply, album image value is not an object."_s);
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_image = value_image.toObject();
|
||||
if (!obj_image.contains("#text"_L1) || !obj_image.contains("size"_L1)) {
|
||||
Error(u"Invalid Json reply, album image value is missing #text or size."_s, obj_image);
|
||||
const QJsonObject object_image = value_image.toObject();
|
||||
if (!object_image.contains("#text"_L1) || !object_image.contains("size"_L1)) {
|
||||
Error(u"Invalid Json reply, album image value is missing #text or size."_s, object_image);
|
||||
continue;
|
||||
}
|
||||
QString image_url = obj_image["#text"_L1].toString();
|
||||
const QString image_url = object_image["#text"_L1].toString();
|
||||
if (image_url.isEmpty()) continue;
|
||||
LastFmImageSize image_size = ImageSizeFromString(obj_image["size"_L1].toString().toLower());
|
||||
const LastFmImageSize image_size = ImageSizeFromString(object_image["size"_L1].toString().toLower());
|
||||
if (image_url_use.isEmpty() || image_size > image_size_use) {
|
||||
image_url_use = image_url;
|
||||
image_size_use = image_size;
|
||||
@@ -275,7 +294,7 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons
|
||||
if (image_url_use.contains("/300x300/"_L1)) {
|
||||
image_url_use = image_url_use.replace("/300x300/"_L1, "/740x0/"_L1);
|
||||
}
|
||||
QUrl url(image_url_use);
|
||||
const QUrl url(image_url_use);
|
||||
if (!url.isValid()) continue;
|
||||
|
||||
CoverProviderSearchResult cover_result;
|
||||
@@ -285,50 +304,6 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons
|
||||
cover_result.image_size = QSize(300, 300);
|
||||
results << cover_result;
|
||||
}
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
|
||||
}
|
||||
|
||||
QByteArray LastFmCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
QByteArray data;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
data = reply->readAll();
|
||||
}
|
||||
else {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "error" and "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("error"_L1) && json_obj.contains("message"_L1)) {
|
||||
int code = json_obj["error"_L1].toInt();
|
||||
QString message = json_obj["message"_L1].toString();
|
||||
error = "Error: "_L1 + QString::number(code) + ": "_L1 + message;
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -22,12 +22,8 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
@@ -40,13 +36,17 @@ class LastFmCoverProvider : public JsonCoverProvider {
|
||||
|
||||
public:
|
||||
explicit LastFmCoverProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||
~LastFmCoverProvider() override;
|
||||
|
||||
bool authentication_required() const override { return true; }
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void QueryFinished(QNetworkReply *reply, const int id, const QString &type);
|
||||
|
||||
protected:
|
||||
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
enum class LastFmImageSize {
|
||||
Unknown,
|
||||
@@ -56,12 +56,8 @@ class LastFmCoverProvider : public JsonCoverProvider {
|
||||
ExtraLarge = 300
|
||||
};
|
||||
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
static LastFmImageSize ImageSizeFromString(const QString &size);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant()) override;
|
||||
|
||||
private:
|
||||
QList<QNetworkReply*> replies_;
|
||||
};
|
||||
|
||||
#endif // LASTFMCOVERPROVIDER_H
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -19,22 +19,18 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QTimer>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
@@ -62,17 +58,6 @@ MusicbrainzCoverProvider::MusicbrainzCoverProvider(const SharedPtr<NetworkAccess
|
||||
|
||||
}
|
||||
|
||||
MusicbrainzCoverProvider::~MusicbrainzCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
@@ -92,19 +77,12 @@ bool MusicbrainzCoverProvider::StartSearch(const QString &artist, const QString
|
||||
|
||||
void MusicbrainzCoverProvider::SendSearchRequest(const SearchRequest &request) {
|
||||
|
||||
QString query = QStringLiteral("release:\"%1\" AND artist:\"%2\"").arg(request.album.trimmed().replace(u'"', "\""_L1), request.artist.trimmed().replace(u'"', "\""_L1));
|
||||
|
||||
const QString query = QStringLiteral("release:\"%1\" AND artist:\"%2\"").arg(request.album.trimmed().replace(u'"', "\""_L1), request.artist.trimmed().replace(u'"', "\""_L1));
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem(u"query"_s, query);
|
||||
url_query.addQueryItem(u"limit"_s, QString::number(kLimit));
|
||||
url_query.addQueryItem(u"fmt"_s, u"json"_s);
|
||||
|
||||
QUrl url(QString::fromLatin1(kReleaseSearchUrl));
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kReleaseSearchUrl)), url_query);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { HandleSearchReply(reply, request.id); });
|
||||
|
||||
}
|
||||
@@ -120,6 +98,55 @@ void MusicbrainzCoverProvider::FlushRequests() {
|
||||
|
||||
}
|
||||
|
||||
JsonBaseRequest::JsonObjectResult MusicbrainzCoverProvider::ParseJsonObject(QNetworkReply *reply) {
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
|
||||
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.contains("help"_L1)) {
|
||||
const QString error = json_object["error"_L1].toString();
|
||||
const QString help = json_object["help"_L1].toString();
|
||||
result.error_code = ErrorCode::APIError;
|
||||
result.error_message = QStringLiteral("%1 (%2)").arg(error, help);
|
||||
}
|
||||
else {
|
||||
result.json_object = json_document.object();
|
||||
}
|
||||
}
|
||||
else {
|
||||
result.error_code = ErrorCode::ParseError;
|
||||
result.error_message = json_parse_error.errorString();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int search_id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
@@ -128,41 +155,33 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
reply->deleteLater();
|
||||
|
||||
CoverProviderSearchResults results;
|
||||
const QScopeGuard search_finished = qScopeGuard([this, search_id, &results]() { Q_EMIT SearchFinished(search_id, results); });
|
||||
|
||||
QByteArray data = GetReplyData(reply);
|
||||
if (data.isEmpty()) {
|
||||
Q_EMIT SearchFinished(search_id, results);
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.isEmpty()) {
|
||||
Q_EMIT SearchFinished(search_id, results);
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
if (json_object.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("releases"_L1)) {
|
||||
if (json_obj.contains("error"_L1)) {
|
||||
QString error = json_obj["error"_L1].toString();
|
||||
Error(error);
|
||||
}
|
||||
else {
|
||||
Error(u"Json reply is missing releases."_s, json_obj);
|
||||
}
|
||||
Q_EMIT SearchFinished(search_id, results);
|
||||
if (!json_object.contains("releases"_L1)) {
|
||||
Error(u"Json reply is missing releases."_s, json_object);
|
||||
return;
|
||||
}
|
||||
QJsonValue value_releases = json_obj["releases"_L1];
|
||||
|
||||
const QJsonValue value_releases = json_object["releases"_L1];
|
||||
|
||||
if (!value_releases.isArray()) {
|
||||
Error(u"Json releases is not an array."_s, value_releases);
|
||||
Q_EMIT SearchFinished(search_id, results);
|
||||
return;
|
||||
}
|
||||
const QJsonArray array_releases = value_releases.toArray();
|
||||
|
||||
if (array_releases.isEmpty()) {
|
||||
Q_EMIT SearchFinished(search_id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -172,18 +191,18 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
Error(u"Invalid Json reply, releases array value is not an object."_s);
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_release = value_release.toObject();
|
||||
if (!obj_release.contains("id"_L1) || !obj_release.contains("artist-credit"_L1) || !obj_release.contains("title"_L1)) {
|
||||
Error(u"Invalid Json reply, releases array object is missing id, artist-credit or title."_s, obj_release);
|
||||
const QJsonObject object_release = value_release.toObject();
|
||||
if (!object_release.contains("id"_L1) || !object_release.contains("artist-credit"_L1) || !object_release.contains("title"_L1)) {
|
||||
Error(u"Invalid Json reply, releases array object is missing id, artist-credit or title."_s, object_release);
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonValue json_artists = obj_release["artist-credit"_L1];
|
||||
if (!json_artists.isArray()) {
|
||||
Error(u"Invalid Json reply, artist-credit is not a array."_s, json_artists);
|
||||
const QJsonValue value_artists = object_release["artist-credit"_L1];
|
||||
if (!value_artists.isArray()) {
|
||||
Error(u"Invalid Json reply, artist-credit is not a array."_s, value_artists);
|
||||
continue;
|
||||
}
|
||||
const QJsonArray array_artists = json_artists.toArray();
|
||||
const QJsonArray array_artists = value_artists.toArray();
|
||||
int i = 0;
|
||||
QString artist;
|
||||
for (const QJsonValue &value_artist : array_artists) {
|
||||
@@ -191,18 +210,18 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
Error(u"Invalid Json reply, artist is not a object."_s);
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_artist = value_artist.toObject();
|
||||
const QJsonObject object_artist = value_artist.toObject();
|
||||
|
||||
if (!obj_artist.contains("artist"_L1)) {
|
||||
Error(u"Invalid Json reply, artist is missing."_s, obj_artist);
|
||||
if (!object_artist.contains("artist"_L1)) {
|
||||
Error(u"Invalid Json reply, artist is missing."_s, object_artist);
|
||||
continue;
|
||||
}
|
||||
QJsonValue value_artist2 = obj_artist["artist"_L1];
|
||||
const QJsonValue value_artist2 = object_artist["artist"_L1];
|
||||
if (!value_artist2.isObject()) {
|
||||
Error(u"Invalid Json reply, artist is not an object."_s, value_artist2);
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_artist2 = value_artist2.toObject();
|
||||
const QJsonObject obj_artist2 = value_artist2.toObject();
|
||||
|
||||
if (!obj_artist2.contains("name"_L1)) {
|
||||
Error(u"Invalid Json reply, artist is missing name."_s, value_artist2);
|
||||
@@ -213,59 +232,16 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
}
|
||||
if (i > 1) artist = "Various artists"_L1;
|
||||
|
||||
QString id = obj_release["id"_L1].toString();
|
||||
QString album = obj_release["title"_L1].toString();
|
||||
const QString id = object_release["id"_L1].toString();
|
||||
const QString album = object_release["title"_L1].toString();
|
||||
|
||||
CoverProviderSearchResult cover_result;
|
||||
QUrl url(QString::fromLatin1(kAlbumCoverUrl).arg(id));
|
||||
const QUrl url(QString::fromLatin1(kAlbumCoverUrl).arg(id));
|
||||
cover_result.artist = artist;
|
||||
cover_result.album = album;
|
||||
cover_result.image_url = url;
|
||||
results.append(cover_result);
|
||||
}
|
||||
Q_EMIT SearchFinished(search_id, results);
|
||||
|
||||
}
|
||||
|
||||
QByteArray MusicbrainzCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
QByteArray data;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
data = reply->readAll();
|
||||
}
|
||||
else {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
QString failure_reason = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
Error(failure_reason);
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "error" - 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("error"_L1)) {
|
||||
error = json_obj["error"_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();
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-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
|
||||
@@ -22,13 +22,9 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QQueue>
|
||||
#include <QByteArray>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
@@ -42,7 +38,6 @@ class MusicbrainzCoverProvider : public JsonCoverProvider {
|
||||
|
||||
public:
|
||||
explicit MusicbrainzCoverProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||
~MusicbrainzCoverProvider() override;
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override;
|
||||
|
||||
@@ -59,13 +54,12 @@ class MusicbrainzCoverProvider : public JsonCoverProvider {
|
||||
};
|
||||
|
||||
void SendSearchRequest(const SearchRequest &request);
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant()) override;
|
||||
|
||||
private:
|
||||
QTimer *timer_flush_requests_;
|
||||
QQueue<SearchRequest> queue_search_requests_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
};
|
||||
|
||||
#endif // MUSICBRAINZCOVERPROVIDER_H
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2020-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
|
||||
@@ -19,20 +19,20 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QRegularExpression>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonObject>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "utilities/musixmatchprovider.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
@@ -40,37 +40,23 @@
|
||||
#include "musixmatchcoverprovider.h"
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
using namespace MusixmatchProvider;
|
||||
|
||||
MusixmatchCoverProvider::MusixmatchCoverProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
||||
: JsonCoverProvider(u"Musixmatch"_s, true, false, 1.0, true, false, network, parent) {}
|
||||
|
||||
MusixmatchCoverProvider::~MusixmatchCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MusixmatchCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
Q_UNUSED(title);
|
||||
|
||||
if (artist.isEmpty() || album.isEmpty()) return false;
|
||||
|
||||
QString artist_stripped = StringFixup(artist);
|
||||
QString album_stripped = StringFixup(album);
|
||||
const QString artist_stripped = StringFixup(artist);
|
||||
const QString album_stripped = StringFixup(album);
|
||||
|
||||
if (artist_stripped.isEmpty() || album_stripped.isEmpty()) return false;
|
||||
|
||||
QUrl url(QStringLiteral("https://www.musixmatch.com/album/%1/%2").arg(artist_stripped, album_stripped));
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
QNetworkReply *reply = CreateGetRequest(QUrl(QStringLiteral("https://www.musixmatch.com/album/%1/%2").arg(artist_stripped, album_stripped)));
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id, artist, album]() { HandleSearchReply(reply, id, artist, album); });
|
||||
|
||||
//qLog(Debug) << "Musixmatch: Sending request for" << artist_stripped << album_stripped << url;
|
||||
@@ -89,29 +75,19 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
reply->deleteLater();
|
||||
|
||||
CoverProviderSearchResults results;
|
||||
const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results);; });
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
|
||||
Error(QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()));
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
const ReplyDataResult reply_data_result = GetReplyData(reply);
|
||||
if (!reply_data_result.success()) {
|
||||
Error(reply_data_result.error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
Error(u"Empty reply received from server."_s);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
const QByteArray &data = reply_data_result.data;
|
||||
const QString content = QString::fromUtf8(data);
|
||||
const QString data_begin = "<script id=\"__NEXT_DATA__\" type=\"application/json\">"_L1;
|
||||
const QString data_end = "</script>"_L1;
|
||||
if (!content.contains(data_begin) || !content.contains(data_end)) {
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
qint64 begin_idx = content.indexOf(data_begin);
|
||||
@@ -125,89 +101,61 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
}
|
||||
|
||||
if (content_json.isEmpty()) {
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
static const QRegularExpression regex_html_tag(u"<[^>]*>"_s);
|
||||
if (content_json.contains(regex_html_tag)) { // Make sure it's not HTML code.
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(content_json.toUtf8(), &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error(QStringLiteral("Failed to parse json data: %1").arg(error.errorString()));
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
const JsonObjectResult json_object_result = GetJsonObject(content_json.toUtf8());
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
Error(u"Received empty Json document."_s, data);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
QJsonObject json_object = json_object_result.json_object;
|
||||
if (!json_object.contains("props"_L1) || !json_object["props"_L1].isObject()) {
|
||||
Error(u"Json reply is missing props."_s, json_object);
|
||||
return;
|
||||
}
|
||||
json_object = json_object["props"_L1].toObject();
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
Error(u"Json document is not an object."_s, json_doc);
|
||||
if (!json_object.contains("pageProps"_L1) || !json_object["pageProps"_L1].isObject()) {
|
||||
Error(u"Json props is missing pageProps."_s, json_object);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
json_object = json_object["pageProps"_L1].toObject();
|
||||
|
||||
QJsonObject obj_data = json_doc.object();
|
||||
if (obj_data.isEmpty()) {
|
||||
Error(u"Received empty Json object."_s, json_doc);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
if (!json_object.contains("data"_L1) || !json_object["data"_L1].isObject()) {
|
||||
Error(u"Json pageProps is missing data."_s, json_object);
|
||||
return;
|
||||
}
|
||||
json_object = json_object["data"_L1].toObject();
|
||||
|
||||
if (!obj_data.contains("props"_L1) || !obj_data["props"_L1].isObject()) {
|
||||
Error(u"Json reply is missing props."_s, obj_data);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
if (!json_object.contains("albumGet"_L1) || !json_object["albumGet"_L1].isObject()) {
|
||||
Error(u"Json data is missing albumGet."_s, json_object);
|
||||
return;
|
||||
}
|
||||
obj_data = obj_data["props"_L1].toObject();
|
||||
json_object = json_object["albumGet"_L1].toObject();
|
||||
|
||||
if (!obj_data.contains("pageProps"_L1) || !obj_data["pageProps"_L1].isObject()) {
|
||||
Error(u"Json props is missing pageProps."_s, obj_data);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
if (!json_object.contains("data"_L1) || !json_object["data"_L1].isObject()) {
|
||||
Error(u"Json albumGet reply is missing data."_s, json_object);
|
||||
return;
|
||||
}
|
||||
obj_data = obj_data["pageProps"_L1].toObject();
|
||||
|
||||
if (!obj_data.contains("data"_L1) || !obj_data["data"_L1].isObject()) {
|
||||
Error(u"Json pageProps is missing data."_s, obj_data);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
obj_data = obj_data["data"_L1].toObject();
|
||||
|
||||
if (!obj_data.contains("albumGet"_L1) || !obj_data["albumGet"_L1].isObject()) {
|
||||
Error(u"Json data is missing albumGet."_s, obj_data);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
obj_data = obj_data["albumGet"_L1].toObject();
|
||||
|
||||
if (!obj_data.contains("data"_L1) || !obj_data["data"_L1].isObject()) {
|
||||
Error(u"Json albumGet reply is missing data."_s, obj_data);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
obj_data = obj_data["data"_L1].toObject();
|
||||
json_object = json_object["data"_L1].toObject();
|
||||
|
||||
CoverProviderSearchResult result;
|
||||
if (obj_data.contains("artistName"_L1) && obj_data["artistName"_L1].isString()) {
|
||||
result.artist = obj_data["artistName"_L1].toString();
|
||||
if (json_object.contains("artistName"_L1) && json_object["artistName"_L1].isString()) {
|
||||
result.artist = json_object["artistName"_L1].toString();
|
||||
}
|
||||
if (obj_data.contains("name"_L1) && obj_data["name"_L1].isString()) {
|
||||
result.album = obj_data["name"_L1].toString();
|
||||
if (json_object.contains("name"_L1) && json_object["name"_L1].isString()) {
|
||||
result.album = json_object["name"_L1].toString();
|
||||
}
|
||||
|
||||
if (result.artist.compare(artist, Qt::CaseInsensitive) != 0 && result.album.compare(album, Qt::CaseInsensitive) != 0) {
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -216,8 +164,8 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
<< qMakePair(u"coverImage350x350"_s, QSize(350, 350));
|
||||
|
||||
for (const QPair<QString, QSize> &cover_size : cover_sizes) {
|
||||
if (!obj_data.contains(cover_size.first)) continue;
|
||||
QUrl cover_url(obj_data[cover_size.first].toString());
|
||||
if (!json_object.contains(cover_size.first)) continue;
|
||||
const QUrl cover_url(json_object[cover_size.first].toString());
|
||||
if (cover_url.isValid()) {
|
||||
result.image_url = cover_url;
|
||||
result.image_size = cover_size.second;
|
||||
@@ -225,8 +173,6 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
}
|
||||
}
|
||||
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
|
||||
}
|
||||
|
||||
void MusixmatchCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user