Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83c666baf9 | ||
|
|
b9b54e6e96 | ||
|
|
b2ff6240eb | ||
|
|
26a7c74a24 | ||
|
|
a34954ec4a | ||
|
|
349ab62e75 | ||
|
|
65e960f2c5 | ||
|
|
e22fef8ca4 | ||
|
|
3e99045e2c | ||
|
|
4fcade273e | ||
|
|
5eaff0d26e | ||
|
|
5b22f12b4a | ||
|
|
5f85c2e7a5 | ||
|
|
4fb5a7b6bc | ||
|
|
04c6c862c4 | ||
|
|
baec45f742 | ||
|
|
9efdbd2c10 | ||
|
|
d8800b80d5 | ||
|
|
ec715abb0d | ||
|
|
1485801efb | ||
|
|
4f9ac3d33a | ||
|
|
1577ce4d67 | ||
|
|
7eee74a2e9 | ||
|
|
d9e38fb3be | ||
|
|
81cc90e54a | ||
|
|
bd9771a88f | ||
|
|
f5cd81fe09 | ||
|
|
277e2cff59 | ||
|
|
6fa9514059 | ||
|
|
c5e38b71f7 | ||
|
|
3746915ae7 | ||
|
|
21bdf88d09 | ||
|
|
ff032c3cd7 | ||
|
|
c083110051 | ||
|
|
a7dbeb5d76 | ||
|
|
634f6ea9f5 | ||
|
|
f9e4f9a09a | ||
|
|
aab9889174 | ||
|
|
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 |
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
|
||||
178
.github/workflows/build.yml
vendored
178
.github/workflows/build.yml
vendored
@@ -80,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:
|
||||
@@ -193,6 +206,8 @@ jobs:
|
||||
kdsingleapplication-qt6-devel
|
||||
gtest-devel
|
||||
gmock-devel
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -229,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
|
||||
@@ -244,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
|
||||
@@ -277,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
|
||||
@@ -284,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
|
||||
@@ -332,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
|
||||
@@ -379,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:
|
||||
@@ -466,12 +491,21 @@ jobs:
|
||||
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:
|
||||
@@ -504,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:
|
||||
@@ -549,12 +583,21 @@ jobs:
|
||||
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:
|
||||
@@ -588,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:
|
||||
@@ -631,6 +674,7 @@ jobs:
|
||||
libmtp-dev
|
||||
libgpod-dev
|
||||
libxkbcommon-dev
|
||||
libsparsehash-dev
|
||||
qt6-base-dev
|
||||
qt6-base-private-dev
|
||||
qt6-base-dev-tools
|
||||
@@ -639,6 +683,8 @@ jobs:
|
||||
qt6-l10n-tools
|
||||
gstreamer1.0-alsa
|
||||
gstreamer1.0-pulseaudio
|
||||
libkdsingleapplication-qt6-dev
|
||||
rapidjson-dev
|
||||
- name: Install keyboxd
|
||||
if: matrix.ubuntu_version == 'noble'
|
||||
env:
|
||||
@@ -651,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
|
||||
@@ -683,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}
|
||||
@@ -708,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.8
|
||||
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
|
||||
|
||||
|
||||
@@ -729,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 }}
|
||||
@@ -775,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 }}
|
||||
@@ -837,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
|
||||
|
||||
@@ -1196,12 +1246,42 @@ jobs:
|
||||
build-windows-msvc:
|
||||
name: Build Windows MSVC
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [ 'x86', 'x86_64' ]
|
||||
buildtype: [ 'release' ]
|
||||
include:
|
||||
- name: "x86_64 debug"
|
||||
runner: windows-2022
|
||||
arch: x86_64
|
||||
buildtype: debug
|
||||
|
||||
- name: "x86_64 release"
|
||||
runner: windows-2022
|
||||
arch: x86_64
|
||||
buildtype: release
|
||||
|
||||
- name: "x86 debug"
|
||||
runner: windows-2022
|
||||
arch: x86
|
||||
buildtype: debug
|
||||
|
||||
- name: "x86 release"
|
||||
runner: windows-2022
|
||||
arch: x86
|
||||
buildtype: release
|
||||
|
||||
- name: "arm64 debug"
|
||||
runner: windows-11-arm
|
||||
arch: arm64
|
||||
buildtype: debug
|
||||
|
||||
- name: "arm64 release"
|
||||
runner: windows-11-arm
|
||||
arch: arm64
|
||||
buildtype: release
|
||||
|
||||
runs-on: ${{matrix.runner}}
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set prefix path
|
||||
@@ -1314,15 +1394,18 @@ jobs:
|
||||
shell: cmd
|
||||
run: cmake -E make_directory build
|
||||
|
||||
- name: Set ENABLE_WIN32_CONSOLE (debug)
|
||||
if: matrix.buildtype == 'debug'
|
||||
- name: Set ENABLE_WIN32_CONSOLE
|
||||
shell: bash
|
||||
run: echo "win32_console=ON" >> $GITHUB_ENV
|
||||
run: echo "enable_win32_console=$(test "${{matrix.buildtype}}" = "debug" && echo "ON" || echo "OFF")" >> $GITHUB_ENV
|
||||
|
||||
- name: Set ENABLE_WIN32_CONSOLE (release)
|
||||
if: matrix.buildtype == 'release'
|
||||
- name: Set ENABLE_SPOTIFY
|
||||
shell: bash
|
||||
run: echo "win32_console=OFF" >> $GITHUB_ENV
|
||||
run: echo "enable_spotify=$(test -f "${{env.prefix_path_unix}}/lib/gstreamer-1.0/gstspotify.dll" && echo "ON" || echo "OFF")" >> $GITHUB_ENV
|
||||
|
||||
- name: Remove -lm from .pc files
|
||||
if: matrix.arch == 'arm64'
|
||||
shell: bash
|
||||
run: sed -i 's/\-lm$//g' ${{env.prefix_path_unix}}/lib/pkgconfig/*.pc
|
||||
|
||||
- name: Run CMake
|
||||
shell: cmd
|
||||
@@ -1334,14 +1417,14 @@ jobs:
|
||||
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
|
||||
-DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake"
|
||||
-DARCH="${{matrix.arch}}"
|
||||
-DENABLE_WIN32_CONSOLE=${{env.win32_console}}
|
||||
-DENABLE_WIN32_CONSOLE=${{env.enable_win32_console}}
|
||||
-DPKG_CONFIG_EXECUTABLE="${{env.prefix_path_forwardslash}}/bin/pkg-config.exe"
|
||||
-DICU_ROOT="${{env.prefix_path_forwardslash}}"
|
||||
-DENABLE_GIO=OFF
|
||||
-DENABLE_AUDIOCD=OFF
|
||||
-DENABLE_MTP=OFF
|
||||
-DENABLE_GPOD=OFF
|
||||
-DENABLE_SPOTIFY=ON
|
||||
-DENABLE_SPOTIFY=${{env.enable_spotify}}
|
||||
|
||||
- name: Run Make
|
||||
shell: cmd
|
||||
@@ -1410,11 +1493,14 @@ jobs:
|
||||
run: copy ${{env.prefix_path_backslash}}\lib\gstreamer-1.0\*.dll .\gstreamer-plugins\
|
||||
|
||||
- name: Download copydlldeps.sh
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: curl -f -O -L https://raw.githubusercontent.com/strawberrymusicplayer/strawberry-mxe/master/tools/copydlldeps.sh
|
||||
|
||||
- name: Copy dependencies
|
||||
# copydlldeps.sh doesn't work with arm64 binaries.
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: >
|
||||
@@ -1431,6 +1517,12 @@ jobs:
|
||||
-F ./gstreamer-plugins
|
||||
-R ${{env.prefix_path_unix}}/bin
|
||||
|
||||
- name: Copy dependencies
|
||||
if: matrix.arch == 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: cp -v ${{env.prefix_path_unix}}/bin/{avcodec*.dll,avfilter*.dll,avformat*.dll,avutil*.dll,brotlicommon.dll,brotlidec.dll,chromaprint.dll,ebur128.dll,faad-2.dll,fdk-aac.dll,ffi-7.dll,FLAC.dll,freetype*.dll,getopt.dll,gio-2.0-0.dll,glib-2.0-0.dll,gme.dll,gmodule-2.0-0.dll,gobject-2.0-0.dll,gst-discoverer-1.0.exe,gst-launch-1.0.exe,gst-play-1.0.exe,gstadaptivedemux-1.0-0.dll,gstapp-1.0-0.dll,gstaudio-1.0-0.dll,gstbadaudio-1.0-0.dll,gstbase-1.0-0.dll,gstcodecparsers-1.0-0.dll,gstfft-1.0-0.dll,gstisoff-1.0-0.dll,gstmpegts-1.0-0.dll,gstnet-1.0-0.dll,gstpbutils-1.0-0.dll,gstreamer-1.0-0.dll,gstriff-1.0-0.dll,gstrtp-1.0-0.dll,gstrtsp-1.0-0.dll,gstsdp-1.0-0.dll,gsttag-1.0-0.dll,gsturidownloader-1.0-0.dll,gstvideo-1.0-0.dll,gstwinrt-1.0-0.dll,harfbuzz.dll,icudt*.dll,icuin*.dll,icuuc*.dll,intl-8.dll,jpeg62.dll,kdsingleapplication*.dll,libbs2b.dll,libcrypto-3-*.dll,fftw3.dll,libiconv*.dll,liblzma.dll,libmp3lame.dll,libopenmpt.dll,libpng16*.dll,libspeex*.dll,libssl-3-*.dll,libxml2*.dll,mpcdec.dll,mpg123.dll,nghttp2.dll,ogg.dll,opus.dll,orc-0.4-0.dll,pcre2-16*.dll,pcre2-8*.dll,postproc*.dll,psl-5.dll,Qt6Concurrent*.dll,Qt6Core*.dll,Qt6Gui*.dll,Qt6Network*.dll,Qt6Sql*.dll,Qt6Widgets*.dll,qtsparkle-qt6.dll,soup-3.0-0.dll,sqlite3.dll,sqlite3.exe,swresample*.dll,swscale*.dll,tag.dll,vorbis.dll,vorbisfile.dll,wavpackdll.dll,zlib*.dll} .
|
||||
|
||||
- name: Copy nsis files
|
||||
shell: cmd
|
||||
working-directory: build
|
||||
|
||||
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/
|
||||
41
3rdparty/discord-rpc/CMakeLists.txt
vendored
Normal file
41
3rdparty/discord-rpc/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
set(DISCORD_RPC_SOURCES
|
||||
discord_rpc.h
|
||||
discord_register.h
|
||||
discord_rpc.cpp
|
||||
discord_rpc_connection.h
|
||||
discord_rpc_connection.cpp
|
||||
discord_serialization.h
|
||||
discord_serialization.cpp
|
||||
discord_connection.h
|
||||
discord_backoff.h
|
||||
discord_msg_queue.h
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_connection_unix.cpp)
|
||||
if(APPLE)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
|
||||
add_definitions(-DDISCORD_OSX)
|
||||
else()
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
|
||||
add_definitions(-DDISCORD_LINUX)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_connection_win.cpp discord_register_win.cpp)
|
||||
add_definitions(-DDISCORD_WINDOWS)
|
||||
endif()
|
||||
|
||||
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||
endif()
|
||||
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
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) |
|
||||
63
3rdparty/discord-rpc/discord_backoff.h
vendored
Normal file
63
3rdparty/discord-rpc/discord_backoff.h
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_BACKOFF_H
|
||||
#define DISCORD_BACKOFF_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
struct Backoff {
|
||||
int64_t minAmount;
|
||||
int64_t maxAmount;
|
||||
int64_t current;
|
||||
int fails;
|
||||
std::mt19937_64 randGenerator;
|
||||
std::uniform_real_distribution<> randDistribution;
|
||||
|
||||
double rand01() { return randDistribution(randGenerator); }
|
||||
|
||||
Backoff(int64_t min, int64_t max)
|
||||
: minAmount(min), maxAmount(max), current(min), fails(0), randGenerator(static_cast<uint64_t>(time(0))) {
|
||||
}
|
||||
|
||||
void reset() {
|
||||
fails = 0;
|
||||
current = minAmount;
|
||||
}
|
||||
|
||||
int64_t nextDelay() {
|
||||
++fails;
|
||||
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
|
||||
current = std::min(current + delay, maxAmount);
|
||||
return current;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_BACKOFF_H
|
||||
48
3rdparty/discord-rpc/discord_connection.h
vendored
Normal file
48
3rdparty/discord-rpc/discord_connection.h
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_CONNECTION_H
|
||||
#define DISCORD_CONNECTION_H
|
||||
|
||||
// This is to wrap the platform specific kinds of connect/read/write.
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// not really connectiony, but need per-platform
|
||||
int GetProcessId();
|
||||
|
||||
struct BaseConnection {
|
||||
static BaseConnection *Create();
|
||||
static void Destroy(BaseConnection *&);
|
||||
bool isOpen = false;
|
||||
bool Open();
|
||||
bool Close();
|
||||
bool Write(const void *data, size_t length);
|
||||
bool Read(void *data, size_t length);
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_CONNECTION_H
|
||||
160
3rdparty/discord-rpc/discord_connection_unix.cpp
vendored
Normal file
160
3rdparty/discord-rpc/discord_connection_unix.cpp
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_connection.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
int GetProcessId() {
|
||||
return ::getpid();
|
||||
}
|
||||
|
||||
struct BaseConnectionUnix : public BaseConnection {
|
||||
int sock { -1 };
|
||||
};
|
||||
|
||||
static BaseConnectionUnix Connection;
|
||||
static sockaddr_un PipeAddr {};
|
||||
#ifdef MSG_NOSIGNAL
|
||||
static int MsgFlags = MSG_NOSIGNAL;
|
||||
#else
|
||||
static int MsgFlags = 0;
|
||||
#endif
|
||||
|
||||
static const char *GetTempPath() {
|
||||
|
||||
const char *temp = getenv("XDG_RUNTIME_DIR");
|
||||
temp = temp ? temp : getenv("TMPDIR");
|
||||
temp = temp ? temp : getenv("TMP");
|
||||
temp = temp ? temp : getenv("TEMP");
|
||||
temp = temp ? temp : "/tmp";
|
||||
|
||||
return temp;
|
||||
|
||||
}
|
||||
|
||||
BaseConnection *BaseConnection::Create() {
|
||||
PipeAddr.sun_family = AF_UNIX;
|
||||
return &Connection;
|
||||
}
|
||||
|
||||
void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
|
||||
self->Close();
|
||||
c = nullptr;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Open() {
|
||||
|
||||
const char *tempPath = GetTempPath();
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
fcntl(self->sock, F_SETFL, O_NONBLOCK);
|
||||
#ifdef SO_NOSIGPIPE
|
||||
int optval = 1;
|
||||
setsockopt(self->sock, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
|
||||
#endif
|
||||
|
||||
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
|
||||
snprintf(PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
|
||||
int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr));
|
||||
if (err == 0) {
|
||||
self->isOpen = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
self->Close();
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
close(self->sock);
|
||||
self->sock = -1;
|
||||
self->isOpen = false;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Write(const void *data, size_t length) {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t sentBytes = send(self->sock, data, length, MsgFlags);
|
||||
if (sentBytes < 0) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return sentBytes == static_cast<ssize_t>(length);
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Read(void *data, size_t length) {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long res = recv(self->sock, data, length, MsgFlags);
|
||||
if (res < 0) {
|
||||
if (errno == EAGAIN) {
|
||||
return false;
|
||||
}
|
||||
Close();
|
||||
}
|
||||
else if (res == 0) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return static_cast<size_t>(res) == length;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
160
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
Normal file
160
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_connection.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
|
||||
#include <cassert>
|
||||
#include <windows.h>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
int GetProcessId() {
|
||||
return static_cast<int>(::GetCurrentProcessId());
|
||||
}
|
||||
|
||||
struct BaseConnectionWin : public BaseConnection {
|
||||
HANDLE pipe { INVALID_HANDLE_VALUE };
|
||||
};
|
||||
|
||||
static BaseConnectionWin Connection;
|
||||
|
||||
BaseConnection *BaseConnection::Create() {
|
||||
return &Connection;
|
||||
}
|
||||
|
||||
void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(c);
|
||||
self->Close();
|
||||
c = nullptr;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Open() {
|
||||
|
||||
wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" };
|
||||
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
||||
pipeName[pipeDigit] = L'0';
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
for (;;) {
|
||||
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (self->pipe != INVALID_HANDLE_VALUE) {
|
||||
self->isOpen = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto lastError = GetLastError();
|
||||
if (lastError == ERROR_FILE_NOT_FOUND) {
|
||||
if (pipeName[pipeDigit] < L'9') {
|
||||
pipeName[pipeDigit]++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (lastError == ERROR_PIPE_BUSY) {
|
||||
if (!WaitNamedPipeW(pipeName, 10000)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
::CloseHandle(self->pipe);
|
||||
self->pipe = INVALID_HANDLE_VALUE;
|
||||
self->isOpen = false;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Write(const void *data, size_t length) {
|
||||
|
||||
if (length == 0) {
|
||||
return true;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
}
|
||||
if (self->pipe == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
assert(data);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
const DWORD bytesLength = static_cast<DWORD>(length);
|
||||
DWORD bytesWritten = 0;
|
||||
|
||||
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE && bytesWritten == bytesLength;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Read(void *data, size_t length) {
|
||||
|
||||
assert(data);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
}
|
||||
if (self->pipe == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
DWORD bytesAvailable = 0;
|
||||
if (::PeekNamedPipe(self->pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
|
||||
if (bytesAvailable >= length) {
|
||||
DWORD bytesToRead = static_cast<DWORD>(length);
|
||||
DWORD bytesRead = 0;
|
||||
if (::ReadFile(self->pipe, data, bytesToRead, &bytesRead, nullptr) == TRUE) {
|
||||
assert(bytesToRead == bytesRead);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Close();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
64
3rdparty/discord-rpc/discord_msg_queue.h
vendored
Normal file
64
3rdparty/discord-rpc/discord_msg_queue.h
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_MSG_QUEUE_H
|
||||
#define DISCORD_MSG_QUEUE_H
|
||||
|
||||
#include <atomic>
|
||||
|
||||
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
|
||||
// a consumer. Mutex up as needed.
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
template<typename ElementType, std::size_t QueueSize>
|
||||
class MsgQueue {
|
||||
ElementType queue_[QueueSize];
|
||||
std::atomic_uint nextAdd_ { 0 };
|
||||
std::atomic_uint nextSend_ { 0 };
|
||||
std::atomic_uint pendingSends_ { 0 };
|
||||
|
||||
public:
|
||||
MsgQueue() {}
|
||||
|
||||
ElementType *GetNextAddMessage() {
|
||||
// if we are falling behind, bail
|
||||
if (pendingSends_.load() >= QueueSize) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = (nextAdd_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitAdd() { ++pendingSends_; }
|
||||
|
||||
bool HavePendingSends() const { return pendingSends_.load() != 0; }
|
||||
ElementType *GetNextSendMessage() {
|
||||
auto index = (nextSend_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitSend() { --pendingSends_; }
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_MSG_QUEUE_H
|
||||
37
3rdparty/discord-rpc/discord_register.h
vendored
Normal file
37
3rdparty/discord-rpc/discord_register.h
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_REGISTER_H
|
||||
#define DISCORD_REGISTER_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void Discord_Register(const char *applicationId, const char *command);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // DISCORD_REGISTER_H
|
||||
120
3rdparty/discord-rpc/discord_register_linux.cpp
vendored
Normal file
120
3rdparty/discord-rpc/discord_register_linux.cpp
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_register.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <errno.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
static bool Mkdir(const char *path) {
|
||||
int result = mkdir(path, 0755);
|
||||
if (result == 0) {
|
||||
return true;
|
||||
}
|
||||
if (errno == EEXIST) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// We want to register games so we can run them from Discord client as discord-<appid>://
|
||||
extern "C" void Discord_Register(const char *applicationId, const char *command) {
|
||||
|
||||
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
|
||||
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
char exePath[1024]{};
|
||||
if (!command || !command[0]) {
|
||||
const ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
|
||||
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
|
||||
return;
|
||||
}
|
||||
exePath[size] = '\0';
|
||||
command = exePath;
|
||||
}
|
||||
|
||||
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
|
||||
"Name=Game %s\n"
|
||||
"Exec=%s %%u\n" // note: it really wants that %u in there
|
||||
"Type=Application\n"
|
||||
"NoDisplay=true\n"
|
||||
"Categories=Discord;Games;\n"
|
||||
"MimeType=x-scheme-handler/discord-%s;\n";
|
||||
char desktopFile[2048]{};
|
||||
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
|
||||
if (fileLen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char desktopFilename[256]{};
|
||||
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
|
||||
|
||||
char desktopFilePath[1024]{};
|
||||
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/share");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/applications");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, desktopFilename);
|
||||
|
||||
FILE *fp = fopen(desktopFilePath, "w");
|
||||
if (fp) {
|
||||
fwrite(desktopFile, 1, fileLen, fp);
|
||||
fclose(fp);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
char xdgMimeCommand[1024]{};
|
||||
snprintf(xdgMimeCommand,
|
||||
sizeof(xdgMimeCommand),
|
||||
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
|
||||
applicationId,
|
||||
applicationId);
|
||||
if (system(xdgMimeCommand) < 0) {
|
||||
fprintf(stderr, "Failed to register mime handler\n");
|
||||
}
|
||||
|
||||
}
|
||||
99
3rdparty/discord-rpc/discord_register_osx.m
vendored
Normal file
99
3rdparty/discord-rpc/discord_register_osx.m
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#include "discord_register.h"
|
||||
|
||||
static void RegisterCommand(const char *applicationId, const char *command) {
|
||||
|
||||
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
|
||||
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
|
||||
// the command therein (will pass to js's window.open, so requires a url-like thing)
|
||||
|
||||
// Note: will not work for sandboxed apps
|
||||
NSString *home = NSHomeDirectory();
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
|
||||
stringByAppendingPathComponent:@"Application Support"]
|
||||
stringByAppendingPathComponent:@"discord"]
|
||||
stringByAppendingPathComponent:@"games"]
|
||||
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
|
||||
stringByAppendingPathExtension:@"json"];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
|
||||
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
|
||||
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
|
||||
|
||||
}
|
||||
|
||||
static void RegisterURL(const char *applicationId) {
|
||||
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "discord-%s", applicationId);
|
||||
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
|
||||
|
||||
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
|
||||
if (!myBundleId) {
|
||||
fprintf(stderr, "No bundle id found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
|
||||
if (!myURL) {
|
||||
fprintf(stderr, "No bundle url found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Discord_Register(const char *applicationId, const char *command) {
|
||||
|
||||
if (command) {
|
||||
RegisterCommand(applicationId, command);
|
||||
}
|
||||
else {
|
||||
// raii lite
|
||||
@autoreleasepool {
|
||||
RegisterURL(applicationId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
166
3rdparty/discord-rpc/discord_register_win.cpp
vendored
Normal file
166
3rdparty/discord-rpc/discord_register_win.cpp
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_register.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#include <cstdio>
|
||||
|
||||
/**
|
||||
* Updated fixes for MinGW and WinXP
|
||||
* This block is written the way it does not involve changing the rest of the code
|
||||
* Checked to be compiling
|
||||
* 1) strsafe.h belongs to Windows SDK and cannot be added to MinGW
|
||||
* #include guarded, functions redirected to <string.h> substitutes
|
||||
* 2) RegSetKeyValueW and LSTATUS are not declared in <winreg.h>
|
||||
* The entire function is rewritten
|
||||
*/
|
||||
#ifdef __MINGW32__
|
||||
# include <wchar.h>
|
||||
/// strsafe.h fixes
|
||||
static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat, ...) {
|
||||
HRESULT ret;
|
||||
va_list va;
|
||||
va_start(va, pszFormat);
|
||||
cbDest /= 2; // Size is divided by 2 to convert from bytes to wide characters - causes segfault
|
||||
// othervise
|
||||
ret = vsnwprintf(pszDest, cbDest, pszFormat, va);
|
||||
pszDest[cbDest - 1] = 0; // Terminate the string in case a buffer overflow; -1 will be returned
|
||||
va_end(va);
|
||||
return ret;
|
||||
}
|
||||
#else
|
||||
# include <cwchar>
|
||||
# include <strsafe.h>
|
||||
#endif // __MINGW32__
|
||||
|
||||
/// winreg.h fixes
|
||||
#ifndef LSTATUS
|
||||
# define LSTATUS LONG
|
||||
#endif
|
||||
#ifdef RegSetKeyValueW
|
||||
# undefine RegSetKeyValueW
|
||||
#endif
|
||||
#define RegSetKeyValueW regset
|
||||
|
||||
static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) {
|
||||
|
||||
HKEY htkey = hkey, hsubkey = nullptr;
|
||||
LSTATUS ret;
|
||||
if (subkey && subkey[0]) {
|
||||
if ((ret = RegCreateKeyExW(hkey, subkey, 0, 0, 0, KEY_ALL_ACCESS, 0, &hsubkey, 0)) !=
|
||||
ERROR_SUCCESS)
|
||||
return ret;
|
||||
htkey = hsubkey;
|
||||
}
|
||||
ret = RegSetValueExW(htkey, name, 0, type, static_cast<const BYTE*>(data), len);
|
||||
if (hsubkey && hsubkey != hkey)
|
||||
RegCloseKey(hsubkey);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
|
||||
// we want to register games so we can run them as discord-<appid>://
|
||||
// Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions.
|
||||
|
||||
wchar_t exeFilePath[MAX_PATH]{};
|
||||
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
|
||||
wchar_t openCommand[1024]{};
|
||||
|
||||
if (command && command[0]) {
|
||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
|
||||
}
|
||||
else {
|
||||
// StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath);
|
||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
|
||||
}
|
||||
|
||||
wchar_t protocolName[64]{};
|
||||
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
|
||||
wchar_t protocolDescription[128]{};
|
||||
StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
|
||||
wchar_t urlProtocol = 0;
|
||||
|
||||
wchar_t keyName[256]{};
|
||||
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
|
||||
HKEY key;
|
||||
auto status = RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fprintf(stderr, "Error creating key\n");
|
||||
return;
|
||||
}
|
||||
DWORD len;
|
||||
LSTATUS result;
|
||||
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
|
||||
result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing description\n");
|
||||
}
|
||||
|
||||
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
|
||||
result = RegSetKeyValueW(key, nullptr, L"URL Protocol", REG_SZ, &urlProtocol, sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing description\n");
|
||||
}
|
||||
|
||||
result = RegSetKeyValueW(key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing icon\n");
|
||||
}
|
||||
|
||||
len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
|
||||
result = RegSetKeyValueW(key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing command\n");
|
||||
}
|
||||
RegCloseKey(key);
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_Register(const char *applicationId, const char *command) {
|
||||
|
||||
wchar_t appId[32]{};
|
||||
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
|
||||
|
||||
wchar_t openCommand[1024]{};
|
||||
const wchar_t *wcommand = nullptr;
|
||||
if (command && command[0]) {
|
||||
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
|
||||
MultiByteToWideChar(CP_UTF8, 0, command, -1, openCommand, commandBufferLen);
|
||||
wcommand = openCommand;
|
||||
}
|
||||
|
||||
Discord_RegisterW(appId, wcommand);
|
||||
|
||||
}
|
||||
|
||||
510
3rdparty/discord-rpc/discord_rpc.cpp
vendored
Normal file
510
3rdparty/discord-rpc/discord_rpc.cpp
vendored
Normal file
@@ -0,0 +1,510 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_backoff.h"
|
||||
#include "discord_register.h"
|
||||
#include "discord_msg_queue.h"
|
||||
#include "discord_rpc_connection.h"
|
||||
#include "discord_serialization.h"
|
||||
|
||||
using namespace discord_rpc;
|
||||
|
||||
static void Discord_UpdateConnection();
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MaxMessageSize { 16 * 1024 };
|
||||
constexpr size_t MessageQueueSize { 8 };
|
||||
constexpr size_t JoinQueueSize { 8 };
|
||||
|
||||
struct QueuedMessage {
|
||||
size_t length;
|
||||
char buffer[MaxMessageSize];
|
||||
|
||||
void Copy(const QueuedMessage &other) {
|
||||
length = other.length;
|
||||
if (length) {
|
||||
memcpy(buffer, other.buffer, length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct User {
|
||||
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
|
||||
// terminator = 21
|
||||
char userId[32];
|
||||
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
|
||||
// terminator = 129
|
||||
char username[344];
|
||||
// 4 decimal digits + 1 null terminator = 5
|
||||
char discriminator[8];
|
||||
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
|
||||
char avatar[128];
|
||||
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
|
||||
};
|
||||
|
||||
static RpcConnection *Connection { nullptr };
|
||||
static DiscordEventHandlers QueuedHandlers {};
|
||||
static DiscordEventHandlers Handlers {};
|
||||
static std::atomic_bool WasJustConnected { false };
|
||||
static std::atomic_bool WasJustDisconnected { false };
|
||||
static std::atomic_bool GotErrorMessage { false };
|
||||
static std::atomic_bool WasJoinGame { false };
|
||||
static std::atomic_bool WasSpectateGame { false };
|
||||
static std::atomic_bool UpdatePresence { false };
|
||||
static char JoinGameSecret[256];
|
||||
static char SpectateGameSecret[256];
|
||||
static int LastErrorCode { 0 };
|
||||
static char LastErrorMessage[256];
|
||||
static int LastDisconnectErrorCode { 0 };
|
||||
static char LastDisconnectErrorMessage[256];
|
||||
static std::mutex PresenceMutex;
|
||||
static std::mutex HandlerMutex;
|
||||
static QueuedMessage QueuedPresence {};
|
||||
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
||||
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
||||
static User connectedUser;
|
||||
|
||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
|
||||
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
||||
static auto NextConnect = std::chrono::system_clock::now();
|
||||
static int Pid { 0 };
|
||||
static int Nonce { 1 };
|
||||
|
||||
class IoThreadHolder {
|
||||
private:
|
||||
std::atomic_bool keepRunning { true };
|
||||
std::mutex waitForIOMutex;
|
||||
std::condition_variable waitForIOActivity;
|
||||
std::thread ioThread;
|
||||
|
||||
public:
|
||||
void Start() {
|
||||
keepRunning.store(true);
|
||||
ioThread = std::thread([&]() {
|
||||
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
|
||||
Discord_UpdateConnection();
|
||||
while (keepRunning.load()) {
|
||||
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
||||
waitForIOActivity.wait_for(lock, maxWait);
|
||||
Discord_UpdateConnection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Notify() { waitForIOActivity.notify_all(); }
|
||||
|
||||
void Stop() {
|
||||
keepRunning.exchange(false);
|
||||
Notify();
|
||||
if (ioThread.joinable()) {
|
||||
ioThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
~IoThreadHolder() { Stop(); }
|
||||
};
|
||||
|
||||
static IoThreadHolder *IoThread { nullptr };
|
||||
|
||||
static void UpdateReconnectTime() {
|
||||
|
||||
NextConnect = std::chrono::system_clock::now() + std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
|
||||
|
||||
}
|
||||
|
||||
static void SignalIOActivity() {
|
||||
|
||||
if (IoThread != nullptr) {
|
||||
IoThread->Notify();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static bool RegisterForEvent(const char *evtName) {
|
||||
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length = JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
static bool DeregisterForEvent(const char *evtName) {
|
||||
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length = JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void Discord_UpdateConnection() {
|
||||
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Connection->IsOpen()) {
|
||||
if (std::chrono::system_clock::now() >= NextConnect) {
|
||||
UpdateReconnectTime();
|
||||
Connection->Open();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// reads
|
||||
|
||||
for (;;) {
|
||||
JsonDocument message;
|
||||
|
||||
if (!Connection->Read(message)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const char *evtName = GetStrMember(&message, "evt");
|
||||
const char *nonce = GetStrMember(&message, "nonce");
|
||||
|
||||
if (nonce) {
|
||||
// in responses only -- should use to match up response when needed.
|
||||
|
||||
if (evtName && strcmp(evtName, "ERROR") == 0) {
|
||||
auto data = GetObjMember(&message, "data");
|
||||
LastErrorCode = GetIntMember(data, "code");
|
||||
StringCopy(LastErrorMessage, GetStrMember(data, "message", ""));
|
||||
GotErrorMessage.store(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// should have evt == name of event, optional data
|
||||
if (evtName == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto data = GetObjMember(&message, "data");
|
||||
|
||||
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) {
|
||||
auto secret = GetStrMember(data, "secret");
|
||||
if (secret) {
|
||||
StringCopy(JoinGameSecret, secret);
|
||||
WasJoinGame.store(true);
|
||||
}
|
||||
}
|
||||
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) {
|
||||
auto secret = GetStrMember(data, "secret");
|
||||
if (secret) {
|
||||
StringCopy(SpectateGameSecret, secret);
|
||||
WasSpectateGame.store(true);
|
||||
}
|
||||
}
|
||||
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) {
|
||||
auto user = GetObjMember(data, "user");
|
||||
auto userId = GetStrMember(user, "id");
|
||||
auto username = GetStrMember(user, "username");
|
||||
auto avatar = GetStrMember(user, "avatar");
|
||||
auto joinReq = JoinAskQueue.GetNextAddMessage();
|
||||
if (userId && username && joinReq) {
|
||||
StringCopy(joinReq->userId, userId);
|
||||
StringCopy(joinReq->username, username);
|
||||
auto discriminator = GetStrMember(user, "discriminator");
|
||||
if (discriminator) {
|
||||
StringCopy(joinReq->discriminator, discriminator);
|
||||
}
|
||||
if (avatar) {
|
||||
StringCopy(joinReq->avatar, avatar);
|
||||
}
|
||||
else {
|
||||
joinReq->avatar[0] = 0;
|
||||
}
|
||||
JoinAskQueue.CommitAdd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writes
|
||||
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
|
||||
QueuedMessage local;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||
local.Copy(QueuedPresence);
|
||||
}
|
||||
if (!Connection->Write(local.buffer, local.length)) {
|
||||
// if we fail to send, requeue
|
||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||
QueuedPresence.Copy(local);
|
||||
UpdatePresence.exchange(true);
|
||||
}
|
||||
}
|
||||
|
||||
while (SendQueue.HavePendingSends()) {
|
||||
auto qmessage = SendQueue.GetNextSendMessage();
|
||||
Connection->Write(qmessage->buffer, qmessage->length);
|
||||
SendQueue.CommitSend();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
|
||||
|
||||
IoThread = new (std::nothrow) IoThreadHolder();
|
||||
if (IoThread == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoRegister) {
|
||||
Discord_Register(applicationId, nullptr);
|
||||
}
|
||||
|
||||
Pid = GetProcessId();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
|
||||
if (handlers) {
|
||||
QueuedHandlers = *handlers;
|
||||
}
|
||||
else {
|
||||
QueuedHandlers = {};
|
||||
}
|
||||
|
||||
Handlers = {};
|
||||
}
|
||||
|
||||
if (Connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
Connection = RpcConnection::Create(applicationId);
|
||||
Connection->onConnect = [](JsonDocument &readyMessage) {
|
||||
Discord_UpdateHandlers(&QueuedHandlers);
|
||||
if (QueuedPresence.length > 0) {
|
||||
UpdatePresence.exchange(true);
|
||||
SignalIOActivity();
|
||||
}
|
||||
auto data = GetObjMember(&readyMessage, "data");
|
||||
auto user = GetObjMember(data, "user");
|
||||
auto userId = GetStrMember(user, "id");
|
||||
auto username = GetStrMember(user, "username");
|
||||
auto avatar = GetStrMember(user, "avatar");
|
||||
if (userId && username) {
|
||||
StringCopy(connectedUser.userId, userId);
|
||||
StringCopy(connectedUser.username, username);
|
||||
auto discriminator = GetStrMember(user, "discriminator");
|
||||
if (discriminator) {
|
||||
StringCopy(connectedUser.discriminator, discriminator);
|
||||
}
|
||||
if (avatar) {
|
||||
StringCopy(connectedUser.avatar, avatar);
|
||||
}
|
||||
else {
|
||||
connectedUser.avatar[0] = 0;
|
||||
}
|
||||
}
|
||||
WasJustConnected.exchange(true);
|
||||
ReconnectTimeMs.reset();
|
||||
};
|
||||
Connection->onDisconnect = [](int err, const char *message) {
|
||||
LastDisconnectErrorCode = err;
|
||||
StringCopy(LastDisconnectErrorMessage, message);
|
||||
WasJustDisconnected.exchange(true);
|
||||
UpdateReconnectTime();
|
||||
};
|
||||
|
||||
IoThread->Start();
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_Shutdown() {
|
||||
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
Connection->onConnect = nullptr;
|
||||
Connection->onDisconnect = nullptr;
|
||||
Handlers = {};
|
||||
QueuedPresence.length = 0;
|
||||
UpdatePresence.exchange(false);
|
||||
if (IoThread != nullptr) {
|
||||
IoThread->Stop();
|
||||
delete IoThread;
|
||||
IoThread = nullptr;
|
||||
}
|
||||
|
||||
RpcConnection::Destroy(Connection);
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||
QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
|
||||
UpdatePresence.exchange(true);
|
||||
}
|
||||
|
||||
SignalIOActivity();
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_ClearPresence(void) {
|
||||
Discord_UpdatePresence(nullptr);
|
||||
}
|
||||
|
||||
extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) {
|
||||
|
||||
// if we are not connected, let's not batch up stale messages for later
|
||||
if (!Connection || !Connection->IsOpen()) {
|
||||
return;
|
||||
}
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_RunCallbacks() {
|
||||
|
||||
// Note on some weirdness: internally we might connect, get other signals, disconnect any number
|
||||
// of times inbetween calls here. Externally, we want the sequence to seem sane, so any other
|
||||
// signals are book-ended by calls to ready and disconnect.
|
||||
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool wasDisconnected = WasJustDisconnected.exchange(false);
|
||||
const bool isConnected = Connection->IsOpen();
|
||||
|
||||
if (isConnected) {
|
||||
// if we are connected, disconnect cb first
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (wasDisconnected && Handlers.disconnected) {
|
||||
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (WasJustConnected.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.ready) {
|
||||
DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||
Handlers.ready(&du);
|
||||
}
|
||||
}
|
||||
|
||||
if (GotErrorMessage.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.errored) {
|
||||
Handlers.errored(LastErrorCode, LastErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (WasJoinGame.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinGame) {
|
||||
Handlers.joinGame(JoinGameSecret);
|
||||
}
|
||||
}
|
||||
|
||||
if (WasSpectateGame.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.spectateGame) {
|
||||
Handlers.spectateGame(SpectateGameSecret);
|
||||
}
|
||||
}
|
||||
|
||||
// Right now this batches up any requests and sends them all in a burst; I could imagine a world
|
||||
// where the implementer would rather sequentially accept/reject each one before the next invite
|
||||
// is sent. I left it this way because I could also imagine wanting to process these all and
|
||||
// maybe show them in one common dialog and/or start fetching the avatars in parallel, and if
|
||||
// not it should be trivial for the implementer to make a queue themselves.
|
||||
while (JoinAskQueue.HavePendingSends()) {
|
||||
const auto req = JoinAskQueue.GetNextSendMessage();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinRequest) {
|
||||
DiscordUser du { req->userId, req->username, req->discriminator, req->avatar };
|
||||
Handlers.joinRequest(&du);
|
||||
}
|
||||
}
|
||||
JoinAskQueue.CommitSend();
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
// if we are not connected, disconnect message last
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (wasDisconnected && Handlers.disconnected) {
|
||||
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
||||
|
||||
if (newHandlers) {
|
||||
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||
RegisterForEvent(event); \
|
||||
} \
|
||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
||||
DeregisterForEvent(event); \
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
|
||||
HANDLE_EVENT_REGISTRATION(spectateGame, "ACTIVITY_SPECTATE")
|
||||
HANDLE_EVENT_REGISTRATION(joinRequest, "ACTIVITY_JOIN_REQUEST")
|
||||
|
||||
#undef HANDLE_EVENT_REGISTRATION
|
||||
|
||||
Handlers = *newHandlers;
|
||||
}
|
||||
else {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
Handlers = {};
|
||||
}
|
||||
|
||||
}
|
||||
93
3rdparty/discord-rpc/discord_rpc.h
vendored
Normal file
93
3rdparty/discord-rpc/discord_rpc.h
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_RPC_H
|
||||
#define DISCORD_RPC_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct DiscordRichPresence {
|
||||
int type;
|
||||
const char *name; /* max 128 bytes */
|
||||
const char *state; /* max 128 bytes */
|
||||
const char *details; /* max 128 bytes */
|
||||
int64_t startTimestamp;
|
||||
int64_t endTimestamp;
|
||||
const char *largeImageKey; /* max 32 bytes */
|
||||
const char *largeImageText; /* max 128 bytes */
|
||||
const char *smallImageKey; /* max 32 bytes */
|
||||
const char *smallImageText; /* max 128 bytes */
|
||||
const char *partyId; /* max 128 bytes */
|
||||
int partySize;
|
||||
int partyMax;
|
||||
int partyPrivacy;
|
||||
const char *matchSecret; /* max 128 bytes */
|
||||
const char *joinSecret; /* max 128 bytes */
|
||||
const char *spectateSecret; /* max 128 bytes */
|
||||
int8_t instance;
|
||||
} DiscordRichPresence;
|
||||
|
||||
typedef struct DiscordUser {
|
||||
const char *userId;
|
||||
const char *username;
|
||||
const char *discriminator;
|
||||
const char *avatar;
|
||||
} DiscordUser;
|
||||
|
||||
typedef struct DiscordEventHandlers {
|
||||
void (*ready)(const DiscordUser *request);
|
||||
void (*disconnected)(int errorCode, const char *message);
|
||||
void (*errored)(int errorCode, const char *message);
|
||||
void (*joinGame)(const char *joinSecret);
|
||||
void (*spectateGame)(const char *spectateSecret);
|
||||
void (*joinRequest)(const DiscordUser *request);
|
||||
} DiscordEventHandlers;
|
||||
|
||||
#define DISCORD_REPLY_NO 0
|
||||
#define DISCORD_REPLY_YES 1
|
||||
#define DISCORD_REPLY_IGNORE 2
|
||||
#define DISCORD_PARTY_PRIVATE 0
|
||||
#define DISCORD_PARTY_PUBLIC 1
|
||||
|
||||
void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister);
|
||||
void Discord_Shutdown(void);
|
||||
|
||||
// checks for incoming messages, dispatches callbacks
|
||||
void Discord_RunCallbacks(void);
|
||||
|
||||
void Discord_UpdatePresence(const DiscordRichPresence *presence);
|
||||
void Discord_ClearPresence(void);
|
||||
|
||||
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
|
||||
|
||||
void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif // DISCORD_RPC_H
|
||||
168
3rdparty/discord-rpc/discord_rpc_connection.cpp
vendored
Normal file
168
3rdparty/discord-rpc/discord_rpc_connection.cpp
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_rpc_connection.h"
|
||||
#include "discord_serialization.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
static const int RpcVersion = 1;
|
||||
static RpcConnection Instance;
|
||||
|
||||
RpcConnection *RpcConnection::Create(const char *applicationId) {
|
||||
|
||||
Instance.connection = BaseConnection::Create();
|
||||
StringCopy(Instance.appId, applicationId);
|
||||
return &Instance;
|
||||
|
||||
}
|
||||
|
||||
void RpcConnection::Destroy(RpcConnection *&c) {
|
||||
|
||||
c->Close();
|
||||
BaseConnection::Destroy(c->connection);
|
||||
c = nullptr;
|
||||
|
||||
}
|
||||
|
||||
void RpcConnection::Open() {
|
||||
|
||||
if (state == State::Connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == State::Disconnected && !connection->Open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == State::SentHandshake) {
|
||||
JsonDocument message;
|
||||
if (Read(message)) {
|
||||
auto cmd = GetStrMember(&message, "cmd");
|
||||
auto evt = GetStrMember(&message, "evt");
|
||||
if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) {
|
||||
state = State::Connected;
|
||||
if (onConnect) {
|
||||
onConnect(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendFrame.opcode = Opcode::Handshake;
|
||||
sendFrame.length = static_cast<uint32_t>(JsonWriteHandshakeObj(sendFrame.message, sizeof(sendFrame.message), RpcVersion, appId));
|
||||
|
||||
if (connection->Write(&sendFrame, sizeof(MessageFrameHeader) + sendFrame.length)) {
|
||||
state = State::SentHandshake;
|
||||
}
|
||||
else {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RpcConnection::Close() {
|
||||
|
||||
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
|
||||
onDisconnect(lastErrorCode, lastErrorMessage);
|
||||
}
|
||||
connection->Close();
|
||||
state = State::Disconnected;
|
||||
|
||||
}
|
||||
|
||||
bool RpcConnection::Write(const void *data, size_t length) {
|
||||
|
||||
sendFrame.opcode = Opcode::Frame;
|
||||
memcpy(sendFrame.message, data, length);
|
||||
sendFrame.length = static_cast<uint32_t>(length);
|
||||
if (!connection->Write(&sendFrame, sizeof(MessageFrameHeader) + length)) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool RpcConnection::Read(JsonDocument &message) {
|
||||
|
||||
if (state != State::Connected && state != State::SentHandshake) {
|
||||
return false;
|
||||
}
|
||||
MessageFrame readFrame{};
|
||||
for (;;) {
|
||||
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
|
||||
if (!didRead) {
|
||||
if (!connection->isOpen) {
|
||||
lastErrorCode = static_cast<int>(ErrorCode::PipeClosed);
|
||||
StringCopy(lastErrorMessage, "Pipe closed");
|
||||
Close();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (readFrame.length > 0) {
|
||||
didRead = connection->Read(readFrame.message, readFrame.length);
|
||||
if (!didRead) {
|
||||
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
|
||||
StringCopy(lastErrorMessage, "Partial data in frame");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
readFrame.message[readFrame.length] = 0;
|
||||
}
|
||||
|
||||
switch (readFrame.opcode) {
|
||||
case Opcode::Close: {
|
||||
message.ParseInsitu(readFrame.message);
|
||||
lastErrorCode = GetIntMember(&message, "code");
|
||||
StringCopy(lastErrorMessage, GetStrMember(&message, "message", ""));
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
case Opcode::Frame:
|
||||
message.ParseInsitu(readFrame.message);
|
||||
return true;
|
||||
case Opcode::Ping:
|
||||
readFrame.opcode = Opcode::Pong;
|
||||
if (!connection->Write(&readFrame, sizeof(MessageFrameHeader) + readFrame.length)) {
|
||||
Close();
|
||||
}
|
||||
break;
|
||||
case Opcode::Pong:
|
||||
break;
|
||||
case Opcode::Handshake:
|
||||
default:
|
||||
// something bad happened
|
||||
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
|
||||
StringCopy(lastErrorMessage, "Bad ipc frame");
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
88
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
Normal file
88
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_RPC_CONNECTION_H
|
||||
#define DISCORD_RPC_CONNECTION_H
|
||||
|
||||
#include "discord_connection.h"
|
||||
#include "discord_serialization.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much smaller.
|
||||
constexpr size_t MaxRpcFrameSize = 64 * 1024;
|
||||
|
||||
struct RpcConnection {
|
||||
enum class ErrorCode : int {
|
||||
Success = 0,
|
||||
PipeClosed = 1,
|
||||
ReadCorrupt = 2,
|
||||
};
|
||||
|
||||
enum class Opcode : uint32_t {
|
||||
Handshake = 0,
|
||||
Frame = 1,
|
||||
Close = 2,
|
||||
Ping = 3,
|
||||
Pong = 4,
|
||||
};
|
||||
|
||||
struct MessageFrameHeader {
|
||||
Opcode opcode;
|
||||
uint32_t length;
|
||||
};
|
||||
|
||||
struct MessageFrame : public MessageFrameHeader {
|
||||
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
|
||||
};
|
||||
|
||||
enum class State : uint32_t {
|
||||
Disconnected,
|
||||
SentHandshake,
|
||||
AwaitingResponse,
|
||||
Connected,
|
||||
};
|
||||
|
||||
BaseConnection *connection { nullptr };
|
||||
State state { State::Disconnected };
|
||||
void (*onConnect)(JsonDocument &message) { nullptr };
|
||||
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
|
||||
char appId[64] {};
|
||||
int lastErrorCode { 0 };
|
||||
char lastErrorMessage[256] {};
|
||||
RpcConnection::MessageFrame sendFrame;
|
||||
|
||||
static RpcConnection *Create(const char *applicationId);
|
||||
static void Destroy(RpcConnection *&);
|
||||
|
||||
inline bool IsOpen() const { return state == State::Connected; }
|
||||
|
||||
void Open();
|
||||
void Close();
|
||||
bool Write(const void *data, size_t length);
|
||||
bool Read(JsonDocument &message);
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_RPC_CONNECTION_H
|
||||
282
3rdparty/discord-rpc/discord_serialization.cpp
vendored
Normal file
282
3rdparty/discord-rpc/discord_serialization.cpp
vendored
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_serialization.h"
|
||||
#include "discord_connection.h"
|
||||
#include "discord_rpc.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
template<typename T>
|
||||
void NumberToString(char *dest, T number) {
|
||||
|
||||
if (!number) {
|
||||
*dest++ = '0';
|
||||
*dest++ = 0;
|
||||
return;
|
||||
}
|
||||
if (number < 0) {
|
||||
*dest++ = '-';
|
||||
number = -number;
|
||||
}
|
||||
char temp[32];
|
||||
int place = 0;
|
||||
while (number) {
|
||||
auto digit = number % 10;
|
||||
number = number / 10;
|
||||
temp[place++] = '0' + static_cast<char>(digit);
|
||||
}
|
||||
for (--place; place >= 0; --place) {
|
||||
*dest++ = temp[place];
|
||||
}
|
||||
*dest = 0;
|
||||
|
||||
}
|
||||
|
||||
// it's ever so slightly faster to not have to strlen the key
|
||||
template<typename T>
|
||||
void WriteKey(JsonWriter &w, T &k) {
|
||||
w.Key(k, sizeof(T) - 1);
|
||||
}
|
||||
|
||||
struct WriteObject {
|
||||
JsonWriter &writer;
|
||||
WriteObject(JsonWriter &w)
|
||||
: writer(w) {
|
||||
writer.StartObject();
|
||||
}
|
||||
template<typename T>
|
||||
WriteObject(JsonWriter &w, T &name)
|
||||
: writer(w) {
|
||||
WriteKey(writer, name);
|
||||
writer.StartObject();
|
||||
}
|
||||
~WriteObject() { writer.EndObject(); }
|
||||
};
|
||||
|
||||
struct WriteArray {
|
||||
JsonWriter &writer;
|
||||
template<typename T>
|
||||
WriteArray(JsonWriter &w, T &name)
|
||||
: writer(w) {
|
||||
WriteKey(writer, name);
|
||||
writer.StartArray();
|
||||
}
|
||||
~WriteArray() { writer.EndArray(); }
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
|
||||
|
||||
if (value && value[0]) {
|
||||
w.Key(k, sizeof(T) - 1);
|
||||
w.String(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
|
||||
|
||||
WriteKey(writer, "nonce");
|
||||
char nonceBuffer[32];
|
||||
NumberToString(nonceBuffer, nonce);
|
||||
writer.String(nonceBuffer);
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject top(writer);
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
writer.String("SET_ACTIVITY");
|
||||
|
||||
{
|
||||
WriteObject args(writer, "args");
|
||||
|
||||
WriteKey(writer, "pid");
|
||||
writer.Int(pid);
|
||||
|
||||
if (presence != nullptr) {
|
||||
WriteObject activity(writer, "activity");
|
||||
|
||||
if (presence->type >= 0 && presence->type <= 5) {
|
||||
WriteKey(writer, "type");
|
||||
writer.Int(presence->type);
|
||||
}
|
||||
|
||||
WriteOptionalString(writer, "name", presence->name);
|
||||
WriteOptionalString(writer, "state", presence->state);
|
||||
WriteOptionalString(writer, "details", presence->details);
|
||||
|
||||
if (presence->startTimestamp || presence->endTimestamp) {
|
||||
WriteObject timestamps(writer, "timestamps");
|
||||
|
||||
if (presence->startTimestamp) {
|
||||
WriteKey(writer, "start");
|
||||
writer.Int64(presence->startTimestamp);
|
||||
}
|
||||
|
||||
if (presence->endTimestamp) {
|
||||
WriteKey(writer, "end");
|
||||
writer.Int64(presence->endTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
if ((presence->largeImageKey && presence->largeImageKey[0]) ||
|
||||
(presence->largeImageText && presence->largeImageText[0]) ||
|
||||
(presence->smallImageKey && presence->smallImageKey[0]) ||
|
||||
(presence->smallImageText && presence->smallImageText[0])) {
|
||||
WriteObject assets(writer, "assets");
|
||||
WriteOptionalString(writer, "large_image", presence->largeImageKey);
|
||||
WriteOptionalString(writer, "large_text", presence->largeImageText);
|
||||
WriteOptionalString(writer, "small_image", presence->smallImageKey);
|
||||
WriteOptionalString(writer, "small_text", presence->smallImageText);
|
||||
}
|
||||
|
||||
if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
|
||||
presence->partyMax || presence->partyPrivacy) {
|
||||
WriteObject party(writer, "party");
|
||||
WriteOptionalString(writer, "id", presence->partyId);
|
||||
if (presence->partySize && presence->partyMax) {
|
||||
WriteArray size(writer, "size");
|
||||
writer.Int(presence->partySize);
|
||||
writer.Int(presence->partyMax);
|
||||
}
|
||||
|
||||
if (presence->partyPrivacy) {
|
||||
WriteKey(writer, "privacy");
|
||||
writer.Int(presence->partyPrivacy);
|
||||
}
|
||||
}
|
||||
|
||||
if ((presence->matchSecret && presence->matchSecret[0]) ||
|
||||
(presence->joinSecret && presence->joinSecret[0]) ||
|
||||
(presence->spectateSecret && presence->spectateSecret[0])) {
|
||||
WriteObject secrets(writer, "secrets");
|
||||
WriteOptionalString(writer, "match", presence->matchSecret);
|
||||
WriteOptionalString(writer, "join", presence->joinSecret);
|
||||
WriteOptionalString(writer, "spectate", presence->spectateSecret);
|
||||
}
|
||||
|
||||
writer.Key("instance");
|
||||
writer.Bool(presence->instance != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
}
|
||||
|
||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
WriteKey(writer, "v");
|
||||
writer.Int(version);
|
||||
WriteKey(writer, "client_id");
|
||||
writer.String(applicationId);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
writer.String("SUBSCRIBE");
|
||||
|
||||
WriteKey(writer, "evt");
|
||||
writer.String(evtName);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
writer.String("UNSUBSCRIBE");
|
||||
|
||||
WriteKey(writer, "evt");
|
||||
writer.String(evtName);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
WriteObject obj(writer);
|
||||
|
||||
WriteKey(writer, "cmd");
|
||||
if (reply == DISCORD_REPLY_YES) {
|
||||
writer.String("SEND_ACTIVITY_JOIN_INVITE");
|
||||
}
|
||||
else {
|
||||
writer.String("CLOSE_ACTIVITY_JOIN_REQUEST");
|
||||
}
|
||||
|
||||
WriteKey(writer, "args");
|
||||
{
|
||||
WriteObject args(writer);
|
||||
|
||||
WriteKey(writer, "user_id");
|
||||
writer.String(userId);
|
||||
}
|
||||
|
||||
JsonWriteNonce(writer, nonce);
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
213
3rdparty/discord-rpc/discord_serialization.h
vendored
Normal file
213
3rdparty/discord-rpc/discord_serialization.h
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_SERIALIZATION_H
|
||||
#define DISCORD_SERIALIZATION_H
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/stringbuffer.h>
|
||||
#include <rapidjson/writer.h>
|
||||
|
||||
struct DiscordRichPresence;
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// if only there was a standard library function for this
|
||||
template<size_t Len>
|
||||
inline size_t StringCopy(char (&dest)[Len], const char *src) {
|
||||
if (!src || !Len) {
|
||||
return 0;
|
||||
}
|
||||
size_t copied;
|
||||
char *out = dest;
|
||||
for (copied = 1; *src && copied < Len; ++copied) {
|
||||
*out++ = *src++;
|
||||
}
|
||||
*out = 0;
|
||||
return copied - 1;
|
||||
}
|
||||
|
||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
|
||||
|
||||
// Commands
|
||||
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
|
||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
||||
|
||||
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
||||
|
||||
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce);
|
||||
|
||||
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need
|
||||
// to supply some of your own allocators for stuff rather than use the defaults
|
||||
|
||||
class LinearAllocator {
|
||||
public:
|
||||
char *buffer_;
|
||||
char *end_;
|
||||
LinearAllocator() {
|
||||
assert(0); // needed for some default case in rapidjson, should not use
|
||||
}
|
||||
LinearAllocator(char *buffer, size_t size)
|
||||
: buffer_(buffer), end_(buffer + size) {
|
||||
}
|
||||
static const bool kNeedFree = false;
|
||||
void *Malloc(size_t size) {
|
||||
char *res = buffer_;
|
||||
buffer_ += size;
|
||||
if (buffer_ > end_) {
|
||||
buffer_ = res;
|
||||
return nullptr;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
void *Realloc(void *originalPtr, size_t originalSize, size_t newSize) {
|
||||
if (newSize == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
// allocate how much you need in the first place
|
||||
assert(!originalPtr && !originalSize);
|
||||
// unused parameter warning
|
||||
(void)(originalPtr);
|
||||
(void)(originalSize);
|
||||
return Malloc(newSize);
|
||||
}
|
||||
static void Free(void *ptr) {
|
||||
/* shrug */
|
||||
(void)ptr;
|
||||
}
|
||||
};
|
||||
|
||||
template<size_t Size>
|
||||
class FixedLinearAllocator : public LinearAllocator {
|
||||
public:
|
||||
char fixedBuffer_[Size];
|
||||
FixedLinearAllocator()
|
||||
: LinearAllocator(fixedBuffer_, Size) {
|
||||
}
|
||||
static const bool kNeedFree = false;
|
||||
};
|
||||
|
||||
// wonder why this isn't a thing already, maybe I missed it
|
||||
class DirectStringBuffer {
|
||||
public:
|
||||
using Ch = char;
|
||||
char *buffer_;
|
||||
char *end_;
|
||||
char *current_;
|
||||
|
||||
DirectStringBuffer(char *buffer, size_t maxLen)
|
||||
: buffer_(buffer), end_(buffer + maxLen), current_(buffer) {
|
||||
}
|
||||
|
||||
void Put(char c) {
|
||||
if (current_ < end_) {
|
||||
*current_++ = c;
|
||||
}
|
||||
}
|
||||
void Flush() {}
|
||||
size_t GetSize() const { return static_cast<size_t>(current_ - buffer_); }
|
||||
};
|
||||
|
||||
using MallocAllocator = rapidjson::CrtAllocator;
|
||||
using PoolAllocator = rapidjson::MemoryPoolAllocator<MallocAllocator>;
|
||||
using UTF8 = rapidjson::UTF8<char>;
|
||||
// Writer appears to need about 16 bytes per nested object level (with 64bit size_t)
|
||||
using StackAllocator = FixedLinearAllocator<2048>;
|
||||
constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t));
|
||||
using JsonWriterBase =
|
||||
rapidjson::Writer<DirectStringBuffer, UTF8, UTF8, StackAllocator, rapidjson::kWriteNoFlags>;
|
||||
class JsonWriter : public JsonWriterBase {
|
||||
public:
|
||||
DirectStringBuffer stringBuffer_;
|
||||
StackAllocator stackAlloc_;
|
||||
|
||||
JsonWriter(char *dest, size_t maxLen)
|
||||
: JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels), stringBuffer_(dest, maxLen), stackAlloc_() {
|
||||
}
|
||||
|
||||
size_t Size() const { return stringBuffer_.GetSize(); }
|
||||
};
|
||||
|
||||
using JsonDocumentBase = rapidjson::GenericDocument<UTF8, PoolAllocator, StackAllocator>;
|
||||
class JsonDocument : public JsonDocumentBase {
|
||||
public:
|
||||
static const int kDefaultChunkCapacity = 32 * 1024;
|
||||
// json parser will use this buffer first, then allocate more if needed; I seriously doubt we
|
||||
// send any messages that would use all of this, though.
|
||||
char parseBuffer_[32 * 1024];
|
||||
MallocAllocator mallocAllocator_;
|
||||
PoolAllocator poolAllocator_;
|
||||
StackAllocator stackAllocator_;
|
||||
JsonDocument()
|
||||
: JsonDocumentBase(rapidjson::kObjectType,
|
||||
&poolAllocator_,
|
||||
sizeof(stackAllocator_.fixedBuffer_),
|
||||
&stackAllocator_),
|
||||
poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_), stackAllocator_() {
|
||||
}
|
||||
};
|
||||
|
||||
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
|
||||
|
||||
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
|
||||
|
||||
if (obj) {
|
||||
auto member = obj->FindMember(name);
|
||||
if (member != obj->MemberEnd() && member->value.IsObject()) {
|
||||
return &member->value;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
|
||||
|
||||
if (obj) {
|
||||
auto member = obj->FindMember(name);
|
||||
if (member != obj->MemberEnd() && member->value.IsInt()) {
|
||||
return member->value.GetInt();
|
||||
}
|
||||
}
|
||||
|
||||
return notFoundDefault;
|
||||
|
||||
}
|
||||
|
||||
inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
|
||||
|
||||
if (obj) {
|
||||
auto member = obj->FindMember(name);
|
||||
if (member != obj->MemberEnd() && member->value.IsString()) {
|
||||
return member->value.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return notFoundDefault;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_SERIALIZATION_H
|
||||
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
@@ -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>
|
||||
@@ -210,6 +210,10 @@ 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})
|
||||
@@ -247,20 +251,7 @@ if(X11_FOUND)
|
||||
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)
|
||||
@@ -371,6 +362,14 @@ optional_component(QPA_QPLATFORMNATIVEINTERFACE ON "QPA Platform Native Interfac
|
||||
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()
|
||||
@@ -457,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
|
||||
@@ -477,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
|
||||
@@ -586,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
|
||||
@@ -599,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
|
||||
@@ -678,7 +682,6 @@ set(SOURCES
|
||||
src/lyrics/htmllyricsprovider.cpp
|
||||
src/lyrics/ovhlyricsprovider.cpp
|
||||
src/lyrics/lololyricsprovider.cpp
|
||||
src/lyrics/geniuslyricsprovider.cpp
|
||||
src/lyrics/musixmatchlyricsprovider.cpp
|
||||
src/lyrics/chartlyricsprovider.cpp
|
||||
src/lyrics/songlyricscomlyricsprovider.cpp
|
||||
@@ -687,8 +690,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
|
||||
@@ -751,7 +752,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
|
||||
@@ -769,7 +770,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
|
||||
@@ -851,6 +852,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
|
||||
@@ -975,7 +979,6 @@ set(HEADERS
|
||||
src/lyrics/htmllyricsprovider.h
|
||||
src/lyrics/ovhlyricsprovider.h
|
||||
src/lyrics/lololyricsprovider.h
|
||||
src/lyrics/geniuslyricsprovider.h
|
||||
src/lyrics/musixmatchlyricsprovider.h
|
||||
src/lyrics/chartlyricsprovider.h
|
||||
src/lyrics/songlyricscomlyricsprovider.h
|
||||
@@ -1233,6 +1236,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
|
||||
@@ -1477,6 +1490,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)
|
||||
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)
|
||||
@@ -1521,6 +1539,7 @@ target_link_libraries(strawberry_lib PUBLIC
|
||||
$<$<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>
|
||||
@@ -1535,7 +1554,8 @@ 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
|
||||
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
|
||||
51
Changelog
51
Changelog
@@ -2,6 +2,57 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
Version 1.2.11 (2025.05.15):
|
||||
|
||||
* Fixed playlist songs sometimes not updated with new cover.
|
||||
* Fixed context album cover showing even when it's disabled in the setting (#1744).
|
||||
* Fixed crash when dragging songs to a closed playlist (#1741).
|
||||
* Enable startup notify in desktop file.
|
||||
* (Windows|MSVC) Add experimental support for native ARM64 builds.
|
||||
* (Windows|MinGW) Fixed crash on exit.
|
||||
|
||||
Version 1.2.10 (2025.04.18):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed Discord rich presence showing bogus artist and album.
|
||||
* Fixed incorrect ID3v2 comment tag.
|
||||
* (macOS|Windows MSVC) Fixed stuck playback of some streams.
|
||||
|
||||
Enhancements:
|
||||
* Removed Genius lyrics (longer working properly because of website changes).
|
||||
* (macOS|Windows MSVC) Added back Spotify
|
||||
|
||||
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:
|
||||
|
||||
27
README.md
27
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.
|
||||
|
||||

|
||||
|
||||
@@ -53,13 +53,15 @@ Funding developers is a way to contribute to open source projects you appreciate
|
||||
* Edit tags on audio files
|
||||
* Fetch tags from MusicBrainz
|
||||
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
|
||||
* Song lyrics from [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/)
|
||||
* Song lyrics from [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/)
|
||||
* Support for multiple backends
|
||||
* Audio analyzer
|
||||
* 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 7)
|
||||
set(STRAWBERRY_VERSION_PATCH 11)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
|
||||
7
debian/control
vendored
7
debian/control
vendored
@@ -22,6 +22,7 @@ Build-Depends: debhelper-compat (= 12),
|
||||
qt6-tools-dev,
|
||||
qt6-tools-dev-tools,
|
||||
qt6-l10n-tools,
|
||||
libkdsingleapplication-qt6-dev,
|
||||
libgstreamer1.0-dev,
|
||||
libgstreamer-plugins-base1.0-dev,
|
||||
libcdio-dev,
|
||||
@@ -29,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
|
||||
@@ -57,7 +60,7 @@ Description: music player and music collection organizer
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||
- Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<li>Edit tags on audio files</li>
|
||||
<li>Automatically retrieve tags from MusicBrainz</li>
|
||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</li>
|
||||
<li>Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</li>
|
||||
<li>Audio analyzer and equalizer</li>
|
||||
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
||||
<li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li>
|
||||
@@ -51,6 +51,10 @@
|
||||
</screenshots>
|
||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||
<releases>
|
||||
<release version="1.2.11" date="2025-05-15"/>
|
||||
<release version="1.2.10" date="2025-04-18"/>
|
||||
<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"/>
|
||||
|
||||
@@ -13,8 +13,7 @@ TryExec=strawberry
|
||||
Icon=strawberry
|
||||
Terminal=false
|
||||
Categories=AudioVideo;Player;Qt;Audio;
|
||||
Keywords=Audio;Player;
|
||||
StartupNotify=false
|
||||
Keywords=Audio;Player;Clementine;
|
||||
MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
|
||||
StartupWMClass=strawberry
|
||||
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
|
||||
|
||||
2
dist/unix/strawberry.1
vendored
2
dist/unix/strawberry.1
vendored
@@ -29,7 +29,7 @@ Features:
|
||||
.br
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
.br
|
||||
- Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
- Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
.br
|
||||
- Support for multiple backends
|
||||
.br
|
||||
|
||||
10
dist/unix/strawberry.spec.in
vendored
10
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
|
||||
@@ -87,7 +93,7 @@ Features:
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||
- Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||
- Support for multiple backends
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
@@ -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
|
||||
|
||||
|
||||
107
dist/windows/strawberry.nsi.in
vendored
107
dist/windows/strawberry.nsi.in
vendored
@@ -21,6 +21,10 @@
|
||||
!define arch_x64
|
||||
!else if "@ARCH@" == "x86_64-w64-mingw32.shared"
|
||||
!define arch_x64
|
||||
!else if "@ARCH@" == "arm64"
|
||||
!define arch_arm64
|
||||
!else
|
||||
!error "Missing ARCH"
|
||||
!endif
|
||||
|
||||
!ifdef arch_x86
|
||||
@@ -31,6 +35,10 @@
|
||||
!define arch "x64"
|
||||
!endif
|
||||
|
||||
!ifdef arch_arm64
|
||||
!define arch "arm64"
|
||||
!endif
|
||||
|
||||
|
||||
!if "@CMAKE_BUILD_TYPE@" == "Release"
|
||||
!define release
|
||||
@@ -38,6 +46,8 @@
|
||||
!define release
|
||||
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
|
||||
!define debug
|
||||
!else
|
||||
!error "Missing CMAKE_BUILD_TYPE"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
@@ -70,7 +80,7 @@
|
||||
!ifdef arch_x86
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
!ifdef arch_x64 || arch_arm64
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug"
|
||||
!endif
|
||||
!else
|
||||
@@ -80,7 +90,7 @@
|
||||
!ifdef arch_x86
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
!ifdef arch_x64 || arch_arm64
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player"
|
||||
!endif
|
||||
!endif
|
||||
@@ -259,7 +269,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"
|
||||
@@ -367,6 +377,10 @@ Section "Strawberry" Strawberry
|
||||
File "libcrypto-3-x64.dll"
|
||||
File "libssl-3-x64.dll"
|
||||
!endif
|
||||
!ifdef arch_arm64
|
||||
File "libcrypto-3-arm64.dll"
|
||||
File "libssl-3-arm64.dll"
|
||||
!endif
|
||||
|
||||
File "FLAC.dll"
|
||||
File "brotlicommon.dll"
|
||||
@@ -381,7 +395,9 @@ Section "Strawberry" Strawberry
|
||||
File "glib-2.0-0.dll"
|
||||
File "gme.dll"
|
||||
File "gmodule-2.0-0.dll"
|
||||
!ifndef arch_arm64
|
||||
File "gnutls.dll"
|
||||
!endif
|
||||
File "gobject-2.0-0.dll"
|
||||
File "gstadaptivedemux-1.0-0.dll"
|
||||
File "gstapp-1.0-0.dll"
|
||||
@@ -408,7 +424,9 @@ Section "Strawberry" Strawberry
|
||||
File "jpeg62.dll"
|
||||
File "kdsingleapplication-qt6.dll"
|
||||
File "libbs2b.dll"
|
||||
!ifndef arch_arm64
|
||||
File "libfaac_dll.dll"
|
||||
!endif
|
||||
File "liblzma.dll"
|
||||
File "libmp3lame.dll"
|
||||
File "libopenmpt.dll"
|
||||
@@ -434,8 +452,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeex.dll"
|
||||
File "pcre2-8.dll"
|
||||
File "pcre2-16.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolame.dll"
|
||||
File "zlib.dll"
|
||||
!endif
|
||||
File "zlib1.dll"
|
||||
!endif
|
||||
!ifdef debug
|
||||
File "freetyped.dll"
|
||||
@@ -444,8 +464,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeexd.dll"
|
||||
File "pcre2-8d.dll"
|
||||
File "pcre2-16d.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolamed.dll"
|
||||
File "zlibd.dll"
|
||||
!endif
|
||||
File "zlibd1.dll"
|
||||
!endif
|
||||
|
||||
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
|
||||
@@ -458,11 +480,15 @@ Section "Strawberry" Strawberry
|
||||
|
||||
; Common files
|
||||
|
||||
File "icudt76.dll"
|
||||
File "icudt77.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
File "fftw3.dll"
|
||||
!else
|
||||
File "libfftw3-3.dll"
|
||||
!endif
|
||||
!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 +497,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"
|
||||
@@ -526,11 +552,13 @@ Section "GIO modules" gio-modules
|
||||
SetOutPath "$INSTDIR\gio-modules"
|
||||
!ifdef mingw
|
||||
File "/oname=libgiognutls.dll" "gio-modules\libgiognutls.dll"
|
||||
File "/oname=libgioopenssl.dll" "gio-modules\libgioopenssl.dll"
|
||||
!endif
|
||||
!ifdef msvc
|
||||
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
|
||||
!ifdef arch_arm64
|
||||
File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll"
|
||||
!else
|
||||
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
|
||||
!endif
|
||||
!endif
|
||||
SectionEnd
|
||||
|
||||
@@ -596,6 +624,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"
|
||||
@@ -673,7 +702,6 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=gstdirectsound.dll" "gstreamer-plugins\gstdirectsound.dll"
|
||||
File "/oname=gstdsd.dll" "gstreamer-plugins\gstdsd.dll"
|
||||
File "/oname=gstequalizer.dll" "gstreamer-plugins\gstequalizer.dll"
|
||||
File "/oname=gstfaac.dll" "gstreamer-plugins\gstfaac.dll"
|
||||
File "/oname=gstfaad.dll" "gstreamer-plugins\gstfaad.dll"
|
||||
File "/oname=gstfdkaac.dll" "gstreamer-plugins\gstfdkaac.dll"
|
||||
File "/oname=gstflac.dll" "gstreamer-plugins\gstflac.dll"
|
||||
@@ -706,7 +734,6 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=gstspeex.dll" "gstreamer-plugins\gstspeex.dll"
|
||||
File "/oname=gsttaglib.dll" "gstreamer-plugins\gsttaglib.dll"
|
||||
File "/oname=gsttcp.dll" "gstreamer-plugins\gsttcp.dll"
|
||||
File "/oname=gsttwolame.dll" "gstreamer-plugins\gsttwolame.dll"
|
||||
File "/oname=gsttypefindfunctions.dll" "gstreamer-plugins\gsttypefindfunctions.dll"
|
||||
File "/oname=gstudp.dll" "gstreamer-plugins\gstudp.dll"
|
||||
File "/oname=gstvolume.dll" "gstreamer-plugins\gstvolume.dll"
|
||||
@@ -718,6 +745,10 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=gstwavpack.dll" "gstreamer-plugins\gstwavpack.dll"
|
||||
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
|
||||
File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll"
|
||||
!ifndef arch_arm64
|
||||
File "/oname=gstfaac.dll" "gstreamer-plugins\gstfaac.dll"
|
||||
File "/oname=gsttwolame.dll" "gstreamer-plugins\gsttwolame.dll"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
|
||||
!endif
|
||||
@@ -783,7 +814,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"
|
||||
@@ -891,6 +922,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libcrypto-3-x64.dll"
|
||||
Delete "$INSTDIR\libssl-3-x64.dll"
|
||||
!endif
|
||||
!ifdef arch_arm64
|
||||
Delete "$INSTDIR\libcrypto-3-arm64.dll"
|
||||
Delete "$INSTDIR\libssl-3-arm64.dll"
|
||||
!endif
|
||||
|
||||
Delete "$INSTDIR\FLAC.dll"
|
||||
Delete "$INSTDIR\brotlicommon.dll"
|
||||
@@ -905,7 +940,9 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\glib-2.0-0.dll"
|
||||
Delete "$INSTDIR\gme.dll"
|
||||
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\gobject-2.0-0.dll"
|
||||
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstapp-1.0-0.dll"
|
||||
@@ -932,7 +969,9 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\jpeg62.dll"
|
||||
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
|
||||
Delete "$INSTDIR\libbs2b.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\liblzma.dll"
|
||||
Delete "$INSTDIR\libmp3lame.dll"
|
||||
Delete "$INSTDIR\libopenmpt.dll"
|
||||
@@ -958,8 +997,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeex.dll"
|
||||
Delete "$INSTDIR\pcre2-8.dll"
|
||||
Delete "$INSTDIR\pcre2-16.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolame.dll"
|
||||
Delete "$INSTDIR\zlib.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
!endif
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\freetyped.dll"
|
||||
@@ -968,8 +1009,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeexd.dll"
|
||||
Delete "$INSTDIR\pcre2-8d.dll"
|
||||
Delete "$INSTDIR\pcre2-16d.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolamed.dll"
|
||||
Delete "$INSTDIR\zlibd.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\zlibd1.dll"
|
||||
!endif
|
||||
|
||||
!ifdef arch_x86
|
||||
@@ -981,11 +1024,15 @@ Section "Uninstall"
|
||||
|
||||
; Common files
|
||||
|
||||
Delete "$INSTDIR\icudt76.dll"
|
||||
Delete "$INSTDIR\icudt77.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
Delete "$INSTDIR\fftw3.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libfftw3-3.dll"
|
||||
!endif
|
||||
!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 +1041,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"
|
||||
@@ -1015,11 +1062,13 @@ Section "Uninstall"
|
||||
|
||||
!ifdef mingw
|
||||
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
|
||||
Delete "$INSTDIR\gio-modules\libgioopenssl.dll"
|
||||
!endif
|
||||
!ifdef msvc
|
||||
Delete "$INSTDIR\gio-modules\giognutls.dll"
|
||||
!ifdef arch_arm64
|
||||
Delete "$INSTDIR\gio-modules\gioopenssl.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\gio-modules\giognutls.dll"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!ifdef msvc && debug
|
||||
@@ -1052,6 +1101,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"
|
||||
@@ -1131,7 +1181,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstdirectsound.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstdsd.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstequalizer.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstfaad.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstfdkaac.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstflac.dll"
|
||||
@@ -1164,7 +1213,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstspeex.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsttaglib.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsttcp.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsttypefindfunctions.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstudp.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstvolume.dll"
|
||||
@@ -1176,9 +1224,14 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
|
||||
!ifdef arch_x64
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
|
||||
!endif
|
||||
|
||||
!endif ; msvc
|
||||
|
||||
Delete "$INSTDIR\Uninstall.exe"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -623,6 +623,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionTask task(task_manager_, tr("Updating %1 database.").arg(Song::TextForSource(source_)));
|
||||
ScopedTransaction transaction(&db);
|
||||
|
||||
SongList added_songs;
|
||||
@@ -853,6 +854,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 +884,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,55 +28,57 @@
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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_);
|
||||
|
||||
}
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(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;
|
||||
}
|
||||
UpdateStreamMetadata(song_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Song CollectionPlaylistItem::Metadata() const {
|
||||
QVariant CollectionPlaylistItem::DatabaseValue(const DatabaseColumn database_column) const {
|
||||
|
||||
if (HasTemporaryMetadata()) return temp_metadata_;
|
||||
return song_;
|
||||
switch (database_column) {
|
||||
case DatabaseColumn::CollectionId:
|
||||
return song_.id();
|
||||
default:
|
||||
return PlaylistItem::DatabaseValue(database_column);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionPlaylistItem::SetArtManual(const QUrl &cover_url) {
|
||||
|
||||
song_.set_art_manual(cover_url);
|
||||
if (HasTemporaryMetadata()) temp_metadata_.set_art_manual(cover_url);
|
||||
if (HasStreamMetadata()) stream_song_.set_art_manual(cover_url);
|
||||
|
||||
}
|
||||
|
||||
@@ -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,25 +32,23 @@ class SqlRow;
|
||||
|
||||
class CollectionPlaylistItem : public PlaylistItem {
|
||||
public:
|
||||
explicit CollectionPlaylistItem();
|
||||
explicit CollectionPlaylistItem(const Song::Source source);
|
||||
explicit CollectionPlaylistItem(const Song &song);
|
||||
|
||||
Song OriginalMetadata() const override { return song_; }
|
||||
void SetOriginalMetadata(const Song &song) override { song_ = song; }
|
||||
|
||||
QUrl OriginalUrl() const override { return song_.url(); }
|
||||
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
|
||||
|
||||
bool InitFromQuery(const SqlRow &query) override;
|
||||
void Reload() override;
|
||||
|
||||
Song Metadata() const override;
|
||||
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;
|
||||
|
||||
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_;
|
||||
|
||||
@@ -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);
|
||||
@@ -999,6 +999,18 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
|
||||
changes << u"file path"_s;
|
||||
notify_new = true;
|
||||
}
|
||||
if (matching_song.filetype() != new_song.filetype()) {
|
||||
changes << u"filetype"_s;
|
||||
notify_new = true;
|
||||
}
|
||||
if (matching_song.filesize() != new_song.filesize()) {
|
||||
changes << u"filesize"_s;
|
||||
notify_new = true;
|
||||
}
|
||||
if (matching_song.length_nanosec() != new_song.length_nanosec()) {
|
||||
changes << u"length"_s;
|
||||
notify_new = true;
|
||||
}
|
||||
if (matching_song.fingerprint() != new_song.fingerprint()) {
|
||||
changes << u"fingerprint"_s;
|
||||
notify_new = true;
|
||||
@@ -1034,6 +1046,9 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
|
||||
if (matching_song.mtime() != new_song.mtime()) {
|
||||
changes << u"mtime"_s;
|
||||
}
|
||||
if (matching_song.ctime() != new_song.ctime()) {
|
||||
changes << u"ctime"_s;
|
||||
}
|
||||
|
||||
if (changes.isEmpty()) {
|
||||
qLog(Debug) << "Song" << file << "unchanged.";
|
||||
@@ -1082,7 +1097,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -396,7 +396,7 @@ void ContextView::UpdateNoSong() {
|
||||
|
||||
void ContextView::NoSong() {
|
||||
|
||||
if (!widget_album_->isVisible()) {
|
||||
if (!widget_album_->isVisibleTo(this)) {
|
||||
widget_album_->show();
|
||||
}
|
||||
|
||||
@@ -440,11 +440,11 @@ void ContextView::SetSong() {
|
||||
label_stop_summary_->clear();
|
||||
|
||||
bool widget_album_changed = !song_prev_.is_valid();
|
||||
if (action_show_album_->isChecked() && !widget_album_->isVisible()) {
|
||||
if (action_show_album_->isChecked() && !widget_album_->isVisibleTo(this)) {
|
||||
widget_album_->show();
|
||||
widget_album_changed = true;
|
||||
}
|
||||
else if (!action_show_album_->isChecked() && widget_album_->isVisible()) {
|
||||
else if (!action_show_album_->isChecked() && widget_album_->isVisibleTo(this)) {
|
||||
widget_album_->hide();
|
||||
widget_album_changed = true;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -63,7 +64,6 @@
|
||||
#include "covermanager/opentidalcoverprovider.h"
|
||||
|
||||
#include "lyrics/lyricsproviders.h"
|
||||
#include "lyrics/geniuslyricsprovider.h"
|
||||
#include "lyrics/ovhlyricsprovider.h"
|
||||
#include "lyrics/lololyricsprovider.h"
|
||||
#include "lyrics/musixmatchlyricsprovider.h"
|
||||
@@ -172,7 +172,6 @@ class ApplicationImpl {
|
||||
lyrics_providers_([app]() {
|
||||
LyricsProviders *lyrics_providers = new LyricsProviders(app);
|
||||
// Initialize the repository of lyrics providers.
|
||||
lyrics_providers->AddProvider(new GeniusLyricsProvider(lyrics_providers->network()));
|
||||
lyrics_providers->AddProvider(new OVHLyricsProvider(lyrics_providers->network()));
|
||||
lyrics_providers->AddProvider(new LoloLyricsProvider(lyrics_providers->network()));
|
||||
lyrics_providers->AddProvider(new MusixmatchLyricsProvider(lyrics_providers->network()));
|
||||
@@ -208,7 +207,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
|
||||
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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,7 +279,13 @@ 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_WIN32
|
||||
@@ -284,6 +294,9 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
|
||||
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);
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -1485,8 +1515,8 @@ void MainWindow::SendNowPlaying() {
|
||||
|
||||
// Send now playing to scrobble services
|
||||
Playlist *playlist = app_->playlist_manager()->active();
|
||||
if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->Metadata().is_metadata_good()) {
|
||||
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->Metadata());
|
||||
if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->EffectiveMetadata().is_metadata_good()) {
|
||||
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->EffectiveMetadata());
|
||||
ui_->action_love->setEnabled(true);
|
||||
ui_->button_love->setEnabled(true);
|
||||
tray_icon_->LoveStateChanged(true);
|
||||
@@ -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);
|
||||
@@ -1532,9 +1562,9 @@ void MainWindow::TrackSkipped(PlaylistItemPtr item) {
|
||||
|
||||
// If it was a collection item then we have to increment its skipped count in the database.
|
||||
|
||||
if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
|
||||
if (item && item->IsLocalCollectionItem() && item->EffectiveMetadata().id() != -1) {
|
||||
|
||||
Song song = item->Metadata();
|
||||
Song song = item->EffectiveMetadata();
|
||||
const qint64 position = app_->player()->engine()->position_nanosec();
|
||||
const qint64 length = app_->player()->engine()->length_nanosec();
|
||||
const float percentage = (length == 0 ? 1 : static_cast<float>(position) / static_cast<float>(length));
|
||||
@@ -1728,7 +1758,7 @@ void MainWindow::FilePathChanged(const QString &path) {
|
||||
void MainWindow::Seeked(const qint64 microseconds) {
|
||||
|
||||
const qint64 position = microseconds / kUsecPerSec;
|
||||
const qint64 length = app_->player()->GetCurrentItem()->Metadata().length_nanosec() / kNsecPerSec;
|
||||
const qint64 length = app_->player()->GetCurrentItem()->EffectiveMetadata().length_nanosec() / kNsecPerSec;
|
||||
tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
@@ -1744,7 +1774,7 @@ void MainWindow::UpdateTrackPosition() {
|
||||
PlaylistItemPtr item(app_->player()->GetCurrentItem());
|
||||
if (!item) return;
|
||||
|
||||
const qint64 length = (item->Metadata().length_nanosec() / kNsecPerSec);
|
||||
const qint64 length = (item->EffectiveMetadata().length_nanosec() / kNsecPerSec);
|
||||
if (length <= 0) return;
|
||||
const int position = std::floor(static_cast<float>(app_->player()->engine()->position_nanosec()) / static_cast<float>(kNsecPerSec) + 0.5);
|
||||
|
||||
@@ -1758,12 +1788,12 @@ void MainWindow::UpdateTrackPosition() {
|
||||
#endif
|
||||
|
||||
// Send Scrobble
|
||||
if (app_->scrobbler()->enabled() && item->Metadata().is_metadata_good()) {
|
||||
if (app_->scrobbler()->enabled() && item->EffectiveMetadata().is_metadata_good()) {
|
||||
Playlist *playlist = app_->playlist_manager()->active();
|
||||
if (playlist && !playlist->scrobbled()) {
|
||||
const qint64 scrobble_point = (playlist->scrobble_point_nanosec() / kNsecPerSec);
|
||||
if (position >= scrobble_point) {
|
||||
app_->scrobbler()->Scrobble(item->Metadata(), scrobble_point);
|
||||
app_->scrobbler()->Scrobble(item->EffectiveMetadata(), scrobble_point);
|
||||
playlist->set_scrobbled(true);
|
||||
}
|
||||
}
|
||||
@@ -1880,7 +1910,7 @@ void MainWindow::AddToPlaylistFromAction(QAction *action) {
|
||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
|
||||
if (!item) continue;
|
||||
items << item;
|
||||
songs << item->Metadata();
|
||||
songs << item->EffectiveMetadata();
|
||||
}
|
||||
|
||||
// We're creating a new playlist
|
||||
@@ -1959,12 +1989,12 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(src_idx.row());
|
||||
if (!item) continue;
|
||||
|
||||
if (item->Metadata().url().isLocalFile()) ++local_songs;
|
||||
if (item->EffectiveMetadata().url().isLocalFile()) ++local_songs;
|
||||
|
||||
if (item->Metadata().has_cue()) {
|
||||
if (item->EffectiveMetadata().has_cue()) {
|
||||
cue_selected = true;
|
||||
}
|
||||
else if (item->Metadata().IsEditable()) {
|
||||
else if (item->EffectiveMetadata().IsEditable()) {
|
||||
++editable;
|
||||
}
|
||||
|
||||
@@ -2067,7 +2097,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
||||
|
||||
// Is it a collection item?
|
||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
|
||||
if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
|
||||
if (item && item->IsLocalCollectionItem() && item->EffectiveMetadata().id() != -1) {
|
||||
playlist_organize_->setVisible(local_songs > 0 && editable > 0 && !cue_selected);
|
||||
playlist_show_in_collection_->setVisible(true);
|
||||
playlist_open_in_browser_->setVisible(true);
|
||||
@@ -2159,9 +2189,9 @@ void MainWindow::RescanSongs() {
|
||||
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(source_index.row()));
|
||||
if (!item) continue;
|
||||
if (item->IsLocalCollectionItem()) {
|
||||
songs << item->Metadata();
|
||||
songs << item->EffectiveMetadata();
|
||||
}
|
||||
else if (item->Metadata().source() == Song::Source::LocalFile) {
|
||||
else if (item->EffectiveMetadata().source() == Song::Source::LocalFile) {
|
||||
QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
|
||||
app_->playlist_manager()->current()->ItemReload(persistent_index, item->OriginalMetadata(), false);
|
||||
}
|
||||
@@ -2447,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()) {
|
||||
@@ -2566,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);
|
||||
@@ -2788,7 +2823,7 @@ void MainWindow::PlaylistOpenInBrowser() {
|
||||
for (const QModelIndex &proxy_index : proxy_indexes) {
|
||||
const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index);
|
||||
if (!source_index.isValid()) continue;
|
||||
urls << QUrl(source_index.sibling(source_index.row(), static_cast<int>(Playlist::Column::Filename)).data().toString());
|
||||
urls << QUrl(source_index.sibling(source_index.row(), static_cast<int>(Playlist::Column::URL)).data().toString());
|
||||
}
|
||||
|
||||
Utilities::OpenInFileBrowser(urls);
|
||||
@@ -2804,7 +2839,7 @@ void MainWindow::PlaylistCopyUrl() {
|
||||
if (!source_index.isValid()) continue;
|
||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
|
||||
if (!item) continue;
|
||||
urls << item->StreamUrl();
|
||||
urls << item->EffectiveUrl();
|
||||
}
|
||||
|
||||
if (urls.count() > 0) {
|
||||
@@ -2946,7 +2981,7 @@ void MainWindow::OpenSettingsDialog() {
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::OpenSettingsDialogAtPage(SettingsDialog::Page page) {
|
||||
void MainWindow::OpenSettingsDialogAtPage(const SettingsDialog::Page page) {
|
||||
settings_dialog_->OpenAtPage(page);
|
||||
}
|
||||
|
||||
@@ -3197,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);
|
||||
@@ -3286,10 +3321,10 @@ void MainWindow::PlaylistDelete() {
|
||||
for (const QModelIndex &proxy_idx : proxy_indexes) {
|
||||
QModelIndex source_idx = app_->playlist_manager()->current()->filter()->mapToSource(proxy_idx);
|
||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_idx.row());
|
||||
if (!item || !item->Metadata().url().isLocalFile()) continue;
|
||||
QString filename = item->Metadata().url().toLocalFile();
|
||||
if (!item || !item->EffectiveMetadata().url().isLocalFile()) continue;
|
||||
QString filename = item->EffectiveMetadata().url().toLocalFile();
|
||||
if (files.contains(filename)) continue;
|
||||
selected_songs << item->Metadata();
|
||||
selected_songs << item->EffectiveMetadata();
|
||||
files << filename;
|
||||
if (item == app_->player()->GetCurrentItem()) is_current_item = true;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -99,11 +102,24 @@ 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);
|
||||
@@ -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);
|
||||
@@ -296,6 +314,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
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_;
|
||||
@@ -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()) {
|
||||
@@ -288,10 +288,10 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
|
||||
bool is_current = false;
|
||||
bool is_next = false;
|
||||
|
||||
if (result.media_url_ == current_item->Url()) {
|
||||
if (result.media_url_ == current_item->OriginalUrl()) {
|
||||
is_current = true;
|
||||
}
|
||||
else if (has_next_row && next_item->Url() == result.media_url_) {
|
||||
else if (has_next_row && next_item->OriginalUrl() == result.media_url_) {
|
||||
is_next = true;
|
||||
}
|
||||
else {
|
||||
@@ -316,8 +316,8 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
|
||||
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
|
||||
|
||||
Song song;
|
||||
if (is_current) song = current_item->Metadata();
|
||||
else if (is_next) song = next_item->Metadata();
|
||||
if (is_current) song = current_item->EffectiveMetadata();
|
||||
else if (is_next) song = next_item->EffectiveMetadata();
|
||||
|
||||
bool update = false;
|
||||
|
||||
@@ -325,7 +325,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
|
||||
if (
|
||||
(result.stream_url_.isValid())
|
||||
&&
|
||||
(result.stream_url_ != song.url())
|
||||
(result.stream_url_ != song.effective_url())
|
||||
)
|
||||
{
|
||||
song.set_stream_url(result.stream_url_);
|
||||
@@ -371,14 +371,14 @@ 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());
|
||||
qLog(Debug) << "Playing song" << current_item->EffectiveMetadata().title() << result.stream_url_ << "position" << play_offset_nanosec_;
|
||||
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;
|
||||
}
|
||||
else if (is_next && !current_item->Metadata().is_module_music()) {
|
||||
qLog(Debug) << "Preloading next song" << next_item->Metadata().title() << result.stream_url_;
|
||||
engine_->StartPreloading(next_item->Url(), result.stream_url_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec());
|
||||
else if (is_next && !current_item->EffectiveMetadata().is_module_music()) {
|
||||
qLog(Debug) << "Preloading next song" << next_item->EffectiveMetadata().title() << result.stream_url_;
|
||||
engine_->StartPreloading(next_item->OriginalUrl(), result.stream_url_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec());
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -504,8 +504,8 @@ bool Player::HandleStopAfter(const Playlist::AutoScroll autoscroll) {
|
||||
|
||||
void Player::TrackEnded() {
|
||||
|
||||
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->Metadata().id() != -1) {
|
||||
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->Metadata().id());
|
||||
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->EffectiveMetadata().id() != -1) {
|
||||
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->EffectiveMetadata().id());
|
||||
}
|
||||
|
||||
if (HandleStopAfter(Playlist::AutoScroll::Maybe)) return;
|
||||
@@ -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;
|
||||
@@ -554,12 +554,12 @@ void Player::PlayPause(const quint64 offset_nanosec, const Playlist::AutoScroll
|
||||
void Player::UnPause() {
|
||||
|
||||
if (current_item_ && pause_time_.isValid()) {
|
||||
const Song &song = current_item_->Metadata();
|
||||
const Song &song = current_item_->EffectiveMetadata();
|
||||
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:
|
||||
@@ -745,7 +745,7 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
|
||||
Q_EMIT TrackSkipped(current_item_);
|
||||
}
|
||||
|
||||
if (current_item_ && playlist_manager_->active()->has_item_at(index) && current_item_->Metadata().IsOnSameAlbum(playlist_manager_->active()->item_at(index)->Metadata())) {
|
||||
if (current_item_ && playlist_manager_->active()->has_item_at(index) && current_item_->EffectiveMetadata().IsOnSameAlbum(playlist_manager_->active()->item_at(index)->EffectiveMetadata())) {
|
||||
change |= EngineBase::TrackChangeType::SameAlbum;
|
||||
}
|
||||
|
||||
@@ -758,7 +758,7 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
|
||||
}
|
||||
|
||||
current_item_ = playlist_manager_->active()->current_item();
|
||||
const QUrl url = current_item_->StreamUrl();
|
||||
const QUrl url = current_item_->EffectiveUrl();
|
||||
|
||||
if (url_handlers_->CanHandle(url)) {
|
||||
// It's already loading
|
||||
@@ -773,8 +773,8 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
|
||||
HandleLoadResult(url_handler->StartLoading(url));
|
||||
}
|
||||
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());
|
||||
qLog(Debug) << "Playing song" << current_item_->EffectiveMetadata().title() << url << "position" << offset_nanosec;
|
||||
engine_->Play(current_item_->OriginalUrl(), url, pause, change, current_item_->EffectiveMetadata().has_cue(), static_cast<quint64>(current_item_->effective_beginning_nanosec()), current_item_->effective_end_nanosec(), offset_nanosec, current_item_->EffectiveMetadata().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) {
|
||||
@@ -823,8 +823,8 @@ void Player::EngineMetadataReceived(const EngineMetadata &engine_metadata) {
|
||||
const int current_row = playlist_manager_->active()->current_row();
|
||||
if (current_row != -1) {
|
||||
PlaylistItemPtr item = playlist_manager_->active()->current_item();
|
||||
if (item && engine_metadata.media_url == item->Url()) {
|
||||
Song song = item->Metadata();
|
||||
if (item && engine_metadata.media_url == item->OriginalUrl()) {
|
||||
Song song = item->EffectiveMetadata();
|
||||
song.MergeFromEngineMetadata(engine_metadata);
|
||||
playlist_manager_->active()->UpdateItemMetadata(current_row, item, song, true);
|
||||
return;
|
||||
@@ -836,8 +836,8 @@ void Player::EngineMetadataReceived(const EngineMetadata &engine_metadata) {
|
||||
const int next_row = playlist_manager_->active()->next_row();
|
||||
if (next_row != -1) {
|
||||
PlaylistItemPtr next_item = playlist_manager_->active()->item_at(next_row);
|
||||
if (engine_metadata.media_url == next_item->Url()) {
|
||||
Song song = next_item->Metadata();
|
||||
if (engine_metadata.media_url == next_item->OriginalUrl()) {
|
||||
Song song = next_item->EffectiveMetadata();
|
||||
song.MergeFromEngineMetadata(engine_metadata);
|
||||
playlist_manager_->active()->UpdateItemMetadata(next_row, next_item, song, true);
|
||||
}
|
||||
@@ -905,11 +905,11 @@ void Player::PlayWithPause(const quint64 offset_nanosec) {
|
||||
}
|
||||
|
||||
void Player::ShowOSD() {
|
||||
if (current_item_) Q_EMIT ForceShowOSD(current_item_->Metadata(), false);
|
||||
if (current_item_) Q_EMIT ForceShowOSD(current_item_->EffectiveMetadata(), false);
|
||||
}
|
||||
|
||||
void Player::TogglePrettyOSD() {
|
||||
if (current_item_) Q_EMIT ForceShowOSD(current_item_->Metadata(), true);
|
||||
if (current_item_) Q_EMIT ForceShowOSD(current_item_->EffectiveMetadata(), true);
|
||||
}
|
||||
|
||||
void Player::TrackAboutToEnd() {
|
||||
@@ -932,7 +932,7 @@ void Player::TrackAboutToEnd() {
|
||||
|
||||
// If the next track is on the same album (or same cue file),
|
||||
// and the user doesn't want to crossfade between tracks on the same album, then don't do this automatic crossfading.
|
||||
if (engine_->crossfade_same_album() || !has_next_row || !next_item || !current_item_->Metadata().IsOnSameAlbum(next_item->Metadata())) {
|
||||
if (engine_->crossfade_same_album() || !has_next_row || !next_item || !current_item_->EffectiveMetadata().IsOnSameAlbum(next_item->EffectiveMetadata())) {
|
||||
TrackEnded();
|
||||
return;
|
||||
}
|
||||
@@ -941,7 +941,7 @@ void Player::TrackAboutToEnd() {
|
||||
// Crossfade is off, so start preloading the next track, so we don't get a gap between songs.
|
||||
if (!has_next_row || !next_item) return;
|
||||
|
||||
QUrl url = next_item->StreamUrl();
|
||||
QUrl url = next_item->EffectiveUrl();
|
||||
|
||||
// Get the actual track URL rather than the stream URL.
|
||||
if (url_handlers_->CanHandle(url)) {
|
||||
@@ -961,20 +961,20 @@ void Player::TrackAboutToEnd() {
|
||||
case UrlHandler::LoadResult::Type::TrackAvailable:
|
||||
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
|
||||
url = result.stream_url_;
|
||||
Song song = next_item->Metadata();
|
||||
Song song = next_item->EffectiveMetadata();
|
||||
song.set_stream_url(url);
|
||||
next_item->SetTemporaryMetadata(song);
|
||||
next_item->SetStreamMetadata(song);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Preloading any format while currently playing module music is broken in GStreamer.
|
||||
// See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/769
|
||||
if (current_item_ && current_item_->Metadata().is_module_music()) {
|
||||
if (current_item_ && current_item_->EffectiveMetadata().is_module_music()) {
|
||||
return;
|
||||
}
|
||||
|
||||
engine_->StartPreloading(next_item->Url(), url, next_item->Metadata().has_cue(), next_item->effective_beginning_nanosec(), next_item->effective_end_nanosec());
|
||||
engine_->StartPreloading(next_item->OriginalUrl(), url, next_item->EffectiveMetadata().has_cue(), next_item->effective_beginning_nanosec(), next_item->effective_end_nanosec());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ struct Song::Private : public QSharedData {
|
||||
QString artist_sortable_;
|
||||
QString albumartist_sortable_;
|
||||
|
||||
QUrl stream_url_; // Temporary stream url set by the URL handler.
|
||||
QUrl stream_url_; // Temporary stream URL set by the URL handler.
|
||||
|
||||
};
|
||||
|
||||
@@ -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_; }
|
||||
@@ -652,7 +653,7 @@ void Song::set_musicbrainz_disc_id(const TagLib::String &v) { d->musicbrainz_dis
|
||||
void Song::set_musicbrainz_release_group_id(const TagLib::String &v) { d->musicbrainz_release_group_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); }
|
||||
void Song::set_musicbrainz_work_id(const TagLib::String &v) { d->musicbrainz_work_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); }
|
||||
|
||||
const QUrl &Song::effective_stream_url() const { return !d->stream_url_.isEmpty() && d->stream_url_.isValid() ? d->stream_url_ : d->url_; }
|
||||
const QUrl &Song::effective_url() const { return !d->stream_url_.isEmpty() && d->stream_url_.isValid() ? d->stream_url_ : d->url_; }
|
||||
const QString &Song::effective_albumartist() const { return d->albumartist_.isEmpty() ? d->artist_ : d->albumartist_; }
|
||||
const QString &Song::effective_albumartist_sortable() const { return d->albumartist_.isEmpty() ? d->artist_sortable_ : d->albumartist_sortable_; }
|
||||
const QString &Song::effective_album() const { return d->album_.isEmpty() ? d->title_ : d->album_; }
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -915,39 +923,54 @@ bool Song::IsEditable() const {
|
||||
return d->valid_ && d->url_.isValid() && ((d->url_.isLocalFile() && write_tags_supported() && !has_cue()) || d->source_ == Source::Stream);
|
||||
}
|
||||
|
||||
bool Song::IsFileInfoEqual(const Song &other) const {
|
||||
|
||||
return d->beginning_ == other.d->beginning_ &&
|
||||
d->end_ == other.d->end_ &&
|
||||
d->url_ == other.d->url_ &&
|
||||
d->basefilename_ == other.d->basefilename_ &&
|
||||
d->filetype_ == other.d->filetype_ &&
|
||||
d->filesize_ == other.d->filesize_ &&
|
||||
d->mtime_ == other.d->mtime_ &&
|
||||
d->ctime_ == other.d->ctime_ &&
|
||||
d->mtime_ == other.d->mtime_ &&
|
||||
d->stream_url_ == other.d->stream_url_;
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsMetadataEqual(const Song &other) const {
|
||||
|
||||
return d->title_ == other.d->title_ &&
|
||||
d->album_ == other.d->album_ &&
|
||||
d->artist_ == other.d->artist_ &&
|
||||
d->albumartist_ == other.d->albumartist_ &&
|
||||
d->track_ == other.d->track_ &&
|
||||
d->disc_ == other.d->disc_ &&
|
||||
d->year_ == other.d->year_ &&
|
||||
d->originalyear_ == other.d->originalyear_ &&
|
||||
d->genre_ == other.d->genre_ &&
|
||||
d->compilation_ == other.d->compilation_ &&
|
||||
d->composer_ == other.d->composer_ &&
|
||||
d->performer_ == other.d->performer_ &&
|
||||
d->grouping_ == other.d->grouping_ &&
|
||||
d->comment_ == other.d->comment_ &&
|
||||
d->lyrics_ == other.d->lyrics_ &&
|
||||
d->artist_id_ == other.d->artist_id_ &&
|
||||
d->album_id_ == other.d->album_id_ &&
|
||||
d->song_id_ == other.d->song_id_ &&
|
||||
d->beginning_ == other.d->beginning_ &&
|
||||
length_nanosec() == other.length_nanosec() &&
|
||||
d->bitrate_ == other.d->bitrate_ &&
|
||||
d->samplerate_ == other.d->samplerate_ &&
|
||||
d->bitdepth_ == other.d->bitdepth_ &&
|
||||
d->cue_path_ == other.d->cue_path_;
|
||||
d->album_ == other.d->album_ &&
|
||||
d->artist_ == other.d->artist_ &&
|
||||
d->albumartist_ == other.d->albumartist_ &&
|
||||
d->track_ == other.d->track_ &&
|
||||
d->disc_ == other.d->disc_ &&
|
||||
d->year_ == other.d->year_ &&
|
||||
d->originalyear_ == other.d->originalyear_ &&
|
||||
d->genre_ == other.d->genre_ &&
|
||||
d->compilation_ == other.d->compilation_ &&
|
||||
d->composer_ == other.d->composer_ &&
|
||||
d->performer_ == other.d->performer_ &&
|
||||
d->grouping_ == other.d->grouping_ &&
|
||||
d->comment_ == other.d->comment_ &&
|
||||
d->lyrics_ == other.d->lyrics_ &&
|
||||
d->artist_id_ == other.d->artist_id_ &&
|
||||
d->album_id_ == other.d->album_id_ &&
|
||||
d->song_id_ == other.d->song_id_ &&
|
||||
d->beginning_ == other.d->beginning_ &&
|
||||
length_nanosec() == other.length_nanosec() &&
|
||||
d->bitrate_ == other.d->bitrate_ &&
|
||||
d->samplerate_ == other.d->samplerate_ &&
|
||||
d->bitdepth_ == other.d->bitdepth_ &&
|
||||
d->cue_path_ == other.d->cue_path_;
|
||||
}
|
||||
|
||||
bool Song::IsPlayStatisticsEqual(const Song &other) const {
|
||||
|
||||
return d->playcount_ == other.d->playcount_ &&
|
||||
d->skipcount_ == other.d->skipcount_ &&
|
||||
d->lastplayed_ == other.d->lastplayed_;
|
||||
d->skipcount_ == other.d->skipcount_ &&
|
||||
d->lastplayed_ == other.d->lastplayed_;
|
||||
|
||||
}
|
||||
|
||||
@@ -972,42 +995,70 @@ bool Song::IsAcoustIdEqual(const Song &other) const {
|
||||
bool Song::IsMusicBrainzEqual(const Song &other) const {
|
||||
|
||||
return d->musicbrainz_album_artist_id_ == other.d->musicbrainz_album_artist_id_ &&
|
||||
d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ &&
|
||||
d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ &&
|
||||
d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ &&
|
||||
d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ &&
|
||||
d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ &&
|
||||
d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ &&
|
||||
d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ &&
|
||||
d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ &&
|
||||
d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_;
|
||||
d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ &&
|
||||
d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ &&
|
||||
d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ &&
|
||||
d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ &&
|
||||
d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ &&
|
||||
d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ &&
|
||||
d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ &&
|
||||
d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ &&
|
||||
d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_;
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsEBUR128Equal(const Song &other) const {
|
||||
|
||||
return d->ebur128_integrated_loudness_lufs_ == other.d->ebur128_integrated_loudness_lufs_ &&
|
||||
d->ebur128_loudness_range_lu_ == other.d->ebur128_loudness_range_lu_;
|
||||
d->ebur128_loudness_range_lu_ == other.d->ebur128_loudness_range_lu_;
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsArtEqual(const Song &other) const {
|
||||
|
||||
return d->art_embedded_ == other.d->art_embedded_ &&
|
||||
d->art_automatic_ == other.d->art_automatic_ &&
|
||||
d->art_manual_ == other.d->art_manual_ &&
|
||||
d->art_unset_ == other.d->art_unset_;
|
||||
d->art_automatic_ == other.d->art_automatic_ &&
|
||||
d->art_manual_ == other.d->art_manual_ &&
|
||||
d->art_unset_ == other.d->art_unset_;
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsCompilationEqual(const Song &other) const {
|
||||
|
||||
return d->compilation_ == other.d->compilation_ &&
|
||||
d->compilation_detected_ == other.d->compilation_detected_ &&
|
||||
d->compilation_on_ == other.d->compilation_on_ &&
|
||||
d->compilation_off_ == other.d->compilation_off_;
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsSettingsEqual(const Song &other) const {
|
||||
|
||||
return d->source_ == other.d->source_ &&
|
||||
d->directory_id_ == other.d->directory_id_ &&
|
||||
d->unavailable_ == other.d->unavailable_;
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsAllMetadataEqual(const Song &other) const {
|
||||
|
||||
return IsMetadataEqual(other) &&
|
||||
IsPlayStatisticsEqual(other) &&
|
||||
IsRatingEqual(other) &&
|
||||
IsAcoustIdEqual(other) &&
|
||||
IsMusicBrainzEqual(other) &&
|
||||
IsArtEqual(other);
|
||||
IsPlayStatisticsEqual(other) &&
|
||||
IsRatingEqual(other) &&
|
||||
IsAcoustIdEqual(other) &&
|
||||
IsMusicBrainzEqual(other) &&
|
||||
IsArtEqual(other) &&
|
||||
IsEBUR128Equal(other);
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsEqual(const Song &other) const {
|
||||
|
||||
return IsFileInfoEqual(other) &&
|
||||
IsSettingsEqual(other) &&
|
||||
IsAllMetadataEqual(other) &&
|
||||
IsFingerprintEqual(other) &&
|
||||
IsCompilationEqual(other);
|
||||
|
||||
}
|
||||
|
||||
@@ -1027,8 +1078,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 +1384,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) {
|
||||
@@ -1803,7 +1862,7 @@ void Song::ToXesam(QVariantMap *map) const {
|
||||
using mpris::AddMetadataAsList;
|
||||
using mpris::AsMPRISDateTimeType;
|
||||
|
||||
AddMetadata(u"xesam:url"_s, effective_stream_url().toString(), map);
|
||||
AddMetadata(u"xesam:url"_s, effective_url().toString(), map);
|
||||
AddMetadata(u"xesam:title"_s, PrettyTitle(), map);
|
||||
AddMetadataAsList(u"xesam:artist"_s, artist(), map);
|
||||
AddMetadata(u"xesam:album"_s, album(), map);
|
||||
@@ -1831,8 +1890,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 +1968,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;
|
||||
@@ -363,7 +365,7 @@ class Song {
|
||||
void set_musicbrainz_release_group_id(const TagLib::String &v);
|
||||
void set_musicbrainz_work_id(const TagLib::String &v);
|
||||
|
||||
const QUrl &effective_stream_url() const;
|
||||
const QUrl &effective_url() const;
|
||||
const QString &effective_albumartist() const;
|
||||
const QString &effective_albumartist_sortable() const;
|
||||
const QString &effective_album() 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;
|
||||
@@ -427,6 +430,7 @@ class Song {
|
||||
bool IsEditable() const;
|
||||
|
||||
// Comparison functions
|
||||
bool IsFileInfoEqual(const Song &other) const;
|
||||
bool IsMetadataEqual(const Song &other) const;
|
||||
bool IsPlayStatisticsEqual(const Song &other) const;
|
||||
bool IsRatingEqual(const Song &other) const;
|
||||
@@ -435,7 +439,10 @@ class Song {
|
||||
bool IsMusicBrainzEqual(const Song &other) const;
|
||||
bool IsEBUR128Equal(const Song &other) const;
|
||||
bool IsArtEqual(const Song &other) const;
|
||||
bool IsCompilationEqual(const Song &other) const;
|
||||
bool IsSettingsEqual(const Song &other) const;
|
||||
bool IsAllMetadataEqual(const Song &other) const;
|
||||
bool IsEqual(const Song &other) const;
|
||||
|
||||
bool IsOnSameAlbum(const Song &other) const;
|
||||
bool IsSimilar(const Song &other) const;
|
||||
@@ -459,6 +466,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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -22,25 +22,20 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "providers/musixmatchprovider.h"
|
||||
|
||||
class QNetworkReply;
|
||||
class NetworkAccessManager;
|
||||
|
||||
class MusixmatchCoverProvider : public JsonCoverProvider, MusixmatchProvider {
|
||||
class MusixmatchCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MusixmatchCoverProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||
~MusixmatchCoverProvider() override;
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override;
|
||||
void CancelSearch(const int id) override;
|
||||
@@ -50,9 +45,6 @@ class MusixmatchCoverProvider : public JsonCoverProvider, MusixmatchProvider {
|
||||
|
||||
private Q_SLOTS:
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id, const QString &artist, const QString &album);
|
||||
|
||||
private:
|
||||
QList<QNetworkReply*> replies_;
|
||||
};
|
||||
|
||||
#endif // MUSIXMATCHCOVERPROVIDER_H
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2024-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,11 +19,6 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
@@ -36,13 +31,13 @@
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QTimer>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/settings.h"
|
||||
#include "core/song.h"
|
||||
#include "constants/timeconstants.h"
|
||||
#include "core/oauthenticator.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "opentidalcoverprovider.h"
|
||||
@@ -51,8 +46,8 @@ using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
constexpr char kSettingsGroup[] = "OpenTidal";
|
||||
constexpr char kAuthUrl[] = "https://auth.tidal.com/v1/oauth2/token";
|
||||
constexpr char kApiUrl[] = "https://openapi.tidal.com";
|
||||
constexpr char kOAuthAccessTokenUrl[] = "https://auth.tidal.com/v1/oauth2/token";
|
||||
constexpr char kApiUrl[] = "https://openapi.tidal.com/v2";
|
||||
constexpr char kApiClientIdB64[] = "RHBwV3FpTEM4ZFJSV1RJaQ==";
|
||||
constexpr char kApiClientSecretB64[] = "cGk0QmxpclZXQWlteWpBc0RnWmZ5RmVlRzA2b3E1blVBVTljUW1IdFhDST0=";
|
||||
constexpr int kLimit = 10;
|
||||
@@ -63,32 +58,24 @@ using std::make_shared;
|
||||
|
||||
OpenTidalCoverProvider::OpenTidalCoverProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent)
|
||||
: JsonCoverProvider(u"OpenTidal"_s, true, false, 2.5, true, false, network, parent),
|
||||
login_timer_(new QTimer(this)),
|
||||
oauth_(new OAuthenticator(network, this)),
|
||||
timer_flush_requests_(new QTimer(this)),
|
||||
login_in_progress_(false),
|
||||
have_login_(false),
|
||||
login_time_(0),
|
||||
expires_in_(0) {
|
||||
login_in_progress_(false) {
|
||||
|
||||
login_timer_->setSingleShot(true);
|
||||
QObject::connect(login_timer_, &QTimer::timeout, this, &OpenTidalCoverProvider::Login);
|
||||
oauth_->set_settings_group(QLatin1String(kSettingsGroup));
|
||||
oauth_->set_type(OAuthenticator::Type::Client_Credentials);
|
||||
oauth_->set_access_token_url(QUrl(QLatin1String(kOAuthAccessTokenUrl)));
|
||||
oauth_->set_client_id(QString::fromLatin1(QByteArray::fromBase64(kApiClientIdB64)));
|
||||
oauth_->set_client_secret(QString::fromLatin1(QByteArray::fromBase64(kApiClientSecretB64)));
|
||||
oauth_->set_use_local_redirect_server(false);
|
||||
oauth_->set_random_port(false);
|
||||
QObject::connect(oauth_, &OAuthenticator::AuthenticationFinished, this, &OpenTidalCoverProvider::OAuthFinished);
|
||||
|
||||
timer_flush_requests_->setInterval(kRequestsDelay);
|
||||
timer_flush_requests_->setSingleShot(false);
|
||||
QObject::connect(timer_flush_requests_, &QTimer::timeout, this, &OpenTidalCoverProvider::FlushRequests);
|
||||
|
||||
LoadSession();
|
||||
|
||||
}
|
||||
|
||||
OpenTidalCoverProvider::~OpenTidalCoverProvider() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
oauth_->LoadSession();
|
||||
|
||||
}
|
||||
|
||||
@@ -96,7 +83,7 @@ bool OpenTidalCoverProvider::StartSearch(const QString &artist, const QString &a
|
||||
|
||||
if (artist.isEmpty() || album.isEmpty()) return false;
|
||||
|
||||
if (!have_login_ && !login_in_progress_ && QDateTime::currentSecsSinceEpoch() - last_login_attempt_.toSecsSinceEpoch() < 120) {
|
||||
if (!oauth_->authenticated() && !login_in_progress_ && (last_login_attempt_.isValid() && (QDateTime::currentSecsSinceEpoch() - last_login_attempt_.toSecsSinceEpoch()) < 120)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -115,32 +102,10 @@ void OpenTidalCoverProvider::CancelSearch(const int id) {
|
||||
Q_UNUSED(id);
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::LoadSession() {
|
||||
|
||||
Settings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
token_type_ = s.value("token_type").toString();
|
||||
access_token_ = s.value("access_token").toString();
|
||||
expires_in_ = s.value("expires_in", 0).toLongLong();
|
||||
login_time_ = s.value("login_time", 0).toLongLong();
|
||||
s.endGroup();
|
||||
|
||||
if (!token_type_.isEmpty() && !access_token_.isEmpty() && (login_time_ + expires_in_) > (QDateTime::currentSecsSinceEpoch() + 30)) {
|
||||
have_login_ = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::FlushRequests() {
|
||||
|
||||
if (have_login_ && (login_time_ + expires_in_) < QDateTime::currentSecsSinceEpoch()) {
|
||||
have_login_ = false;
|
||||
}
|
||||
|
||||
if (!have_login_) {
|
||||
if (!login_in_progress_) {
|
||||
Login();
|
||||
}
|
||||
if (!oauth_->authenticated()) {
|
||||
LoginCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,7 +120,7 @@ void OpenTidalCoverProvider::FlushRequests() {
|
||||
|
||||
void OpenTidalCoverProvider::LoginCheck() {
|
||||
|
||||
if (!login_in_progress_ && (!last_login_attempt_.isValid() || QDateTime::currentSecsSinceEpoch() - last_login_attempt_.toSecsSinceEpoch() > 120)) {
|
||||
if (!oauth_->authenticated() && !login_in_progress_ && (!last_login_attempt_.isValid() || QDateTime::currentSecsSinceEpoch() - last_login_attempt_.toSecsSinceEpoch() > 120)) {
|
||||
Login();
|
||||
}
|
||||
|
||||
@@ -165,156 +130,93 @@ void OpenTidalCoverProvider::Login() {
|
||||
|
||||
qLog(Debug) << "Authenticating...";
|
||||
|
||||
if (login_timer_->isActive()) {
|
||||
login_timer_->stop();
|
||||
}
|
||||
|
||||
have_login_ = false;
|
||||
login_in_progress_ = true;
|
||||
last_login_attempt_ = QDateTime::currentDateTime();
|
||||
|
||||
QUrl url(QString::fromLatin1(kAuthUrl));
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
req.setRawHeader("Authorization", "Basic " + QByteArray(QByteArray::fromBase64(kApiClientIdB64) + ":" + QByteArray::fromBase64(kApiClientSecretB64)).toBase64());
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem(u"grant_type"_s, u"client_credentials"_s);
|
||||
QNetworkReply *reply = network_->post(req, url_query.toString(QUrl::FullyEncoded).toUtf8());
|
||||
replies_ << reply;
|
||||
QObject::connect(reply, &QNetworkReply::sslErrors, this, &OpenTidalCoverProvider::HandleLoginSSLErrors);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { LoginFinished(reply); });
|
||||
oauth_->Authenticate();
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::HandleLoginSSLErrors(const QList<QSslError> &ssl_errors) {
|
||||
|
||||
for (const QSslError &ssl_error : ssl_errors) {
|
||||
qLog(Error) << "OpenTidal:" << ssl_error.errorString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::LoginFinished(QNetworkReply *reply) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
void OpenTidalCoverProvider::OAuthFinished(const bool success, const QString &error) {
|
||||
|
||||
login_in_progress_ = false;
|
||||
last_login_attempt_ = QDateTime();
|
||||
|
||||
QJsonObject json_obj = GetJsonObject(reply);
|
||||
if (json_obj.isEmpty()) {
|
||||
FinishAllSearches();
|
||||
return;
|
||||
if (success) {
|
||||
qLog(Debug) << "OpenTidal: Authentication successful";
|
||||
last_login_attempt_ = QDateTime();
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
}
|
||||
}
|
||||
|
||||
if (!json_obj.contains("access_token"_L1) ||
|
||||
!json_obj.contains("token_type"_L1) ||
|
||||
!json_obj.contains("expires_in"_L1) ||
|
||||
!json_obj["access_token"_L1].isString() ||
|
||||
!json_obj["token_type"_L1].isString()) {
|
||||
qLog(Error) << "OpenTidal: Invalid login reply.";
|
||||
FinishAllSearches();
|
||||
return;
|
||||
else {
|
||||
qLog(Debug) << "OpenTidal: Authentication failed" << error;
|
||||
last_login_attempt_ = QDateTime::currentDateTime();
|
||||
}
|
||||
|
||||
have_login_ = true;
|
||||
token_type_ = json_obj["token_type"_L1].toString();
|
||||
access_token_ = json_obj["access_token"_L1].toString();
|
||||
login_time_ = QDateTime::currentSecsSinceEpoch();
|
||||
expires_in_ = json_obj["expires_in"_L1].toInt();
|
||||
|
||||
Settings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("token_type", token_type_);
|
||||
s.setValue("access_token", access_token_);
|
||||
s.setValue("expires_in", expires_in_);
|
||||
s.setValue("login_time", login_time_);
|
||||
s.endGroup();
|
||||
|
||||
if (expires_in_ <= 300) {
|
||||
expires_in_ = 300;
|
||||
}
|
||||
|
||||
expires_in_ -= 30;
|
||||
|
||||
login_timer_->setInterval(static_cast<int>(expires_in_ * kMsecPerSec));
|
||||
login_timer_->start();
|
||||
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
}
|
||||
|
||||
qLog(Debug) << "Authentication successful";
|
||||
|
||||
}
|
||||
|
||||
QJsonObject OpenTidalCoverProvider::ExtractJsonObj(const QByteArray &data) {
|
||||
JsonBaseRequest::JsonObjectResult OpenTidalCoverProvider::ParseJsonObject(QNetworkReply *reply) {
|
||||
|
||||
QJsonParseError json_parse_error;
|
||||
const QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_parse_error);
|
||||
if (json_parse_error.error != QJsonParseError::NoError) {
|
||||
qLog(Error) << "OpenTidal:" << json_parse_error.errorString();
|
||||
return QJsonObject();
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
if (!json_doc.isObject()) {
|
||||
return QJsonObject();
|
||||
}
|
||||
return json_doc.object();
|
||||
|
||||
}
|
||||
|
||||
QJsonObject OpenTidalCoverProvider::GetJsonObject(QNetworkReply *reply) {
|
||||
|
||||
const int http_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() != QNetworkReply::NoError || (http_code != 200 && http_code != 207)) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qLog(Error) << "OpenTidal:" << reply->errorString();
|
||||
if (reply->error() < 200) {
|
||||
return QJsonObject();
|
||||
}
|
||||
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||
LoginCheck();
|
||||
}
|
||||
}
|
||||
else if (http_code != 200 && http_code != 207) {
|
||||
qLog(Error) << "OpenTidal: Received HTTP code" << http_code;
|
||||
}
|
||||
const QByteArray data = reply->readAll();
|
||||
if (data.isEmpty()) {
|
||||
return QJsonObject();
|
||||
}
|
||||
QJsonObject json_obj = ExtractJsonObj(data);
|
||||
if (json_obj.contains("errors"_L1) && json_obj["errors"_L1].isArray()) {
|
||||
const QJsonArray array = json_obj["errors"_L1].toArray();
|
||||
for (const QJsonValue &value : array) {
|
||||
if (!value.isObject()) continue;
|
||||
QJsonObject obj = value.toObject();
|
||||
if (!obj.contains("category"_L1) ||
|
||||
!obj.contains("code"_L1) ||
|
||||
!obj.contains("detail"_L1)) {
|
||||
continue;
|
||||
}
|
||||
QString category = obj["category"_L1].toString();
|
||||
QString code = obj["code"_L1].toString();
|
||||
QString detail = obj["detail"_L1].toString();
|
||||
qLog(Error) << "OpenTidal:" << category << code << detail;
|
||||
if (category == "AUTHENTICATION_ERROR"_L1) {
|
||||
LoginCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
return QJsonObject();
|
||||
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()) {
|
||||
return QJsonObject();
|
||||
bool clear_session = false;
|
||||
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("errors"_L1) && json_object["errors"_L1].isArray()) {
|
||||
const QJsonArray array_errors = json_object["errors"_L1].toArray();
|
||||
for (const auto &value : array_errors) {
|
||||
if (!value.isObject()) continue;
|
||||
const QJsonObject object_error = value.toObject();
|
||||
if (!object_error.contains("category"_L1) || !object_error.contains("code"_L1) || !object_error.contains("detail"_L1)) {
|
||||
continue;
|
||||
}
|
||||
const QString category = object_error["category"_L1].toString();
|
||||
const QString code = object_error["code"_L1].toString();
|
||||
const QString detail = object_error["detail"_L1].toString();
|
||||
result.error_code = ErrorCode::APIError;
|
||||
result.error_message = QStringLiteral("%1 (%2) (%3)").arg(category, code, detail);
|
||||
if (category == "AUTHENTICATION_ERROR"_L1) {
|
||||
clear_session = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
result.json_object = json_document.object();
|
||||
}
|
||||
}
|
||||
else {
|
||||
result.error_code = ErrorCode::ParseError;
|
||||
result.error_message = json_parse_error.errorString();
|
||||
}
|
||||
}
|
||||
|
||||
return ExtractJsonObj(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);
|
||||
}
|
||||
}
|
||||
|
||||
if (reply->error() == QNetworkReply::AuthenticationRequiredError || clear_session) {
|
||||
oauth_->ClearSession();
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
@@ -331,17 +233,19 @@ void OpenTidalCoverProvider::SendSearchRequest(SearchRequestPtr search_request)
|
||||
}
|
||||
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem(u"query"_s, QString::fromUtf8(QUrl::toPercentEncoding(query)));
|
||||
url_query.addQueryItem(u"include"_s, u"albums"_s);
|
||||
url_query.addQueryItem(u"limit"_s, QString::number(kLimit));
|
||||
url_query.addQueryItem(u"countryCode"_s, u"US"_s);
|
||||
QUrl url(QLatin1String(kApiUrl) + "/search"_L1);
|
||||
QUrl url(QLatin1String(kApiUrl) + "/searchresults/"_L1 + QString::fromUtf8(QUrl::toPercentEncoding(query)));
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/vnd.tidal.v1+json"_s);
|
||||
req.setRawHeader("Authorization", token_type_.toUtf8() + " " + access_token_.toUtf8());
|
||||
QNetworkRequest network_request(url);
|
||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/vnd.tidal.v1+json"_s);
|
||||
if (oauth_->authenticated()) {
|
||||
network_request.setRawHeader("Authorization", oauth_->authorization_header());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
QNetworkReply *reply = network_->get(network_request);
|
||||
replies_ << reply;
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, search_request]() { HandleSearchReply(reply, search_request); });
|
||||
|
||||
@@ -354,92 +258,76 @@ void OpenTidalCoverProvider::HandleSearchReply(QNetworkReply *reply, SearchReque
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject json_obj = GetJsonObject(reply);
|
||||
if (json_obj.isEmpty()) {
|
||||
CoverProviderSearchResults results;
|
||||
const QScopeGuard search_finished = qScopeGuard([this, search_request, &results]() { Q_EMIT SearchFinished(search_request->id, results); });
|
||||
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
if (login_in_progress_) {
|
||||
search_requests_queue_.prepend(search_request);
|
||||
}
|
||||
else {
|
||||
Q_EMIT SearchFinished(search_request->id, CoverProviderSearchResults());
|
||||
}
|
||||
return;
|
||||
}
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
|
||||
if (!json_object.contains("included"_L1) || !json_object["included"_L1].isArray()) {
|
||||
qLog(Error) << "OpenTidal: Json object is missing included.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json_obj.contains("albums"_L1) || !json_obj["albums"_L1].isArray()) {
|
||||
qLog(Debug) << "OpenTidal: Json object is missing albums.";
|
||||
Q_EMIT SearchFinished(search_request->id, CoverProviderSearchResults());
|
||||
const QJsonArray array_included = json_object["included"_L1].toArray();
|
||||
if (array_included.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonArray array_albums = json_obj["albums"_L1].toArray();
|
||||
if (array_albums.isEmpty()) {
|
||||
Q_EMIT SearchFinished(search_request->id, CoverProviderSearchResults());
|
||||
return;
|
||||
}
|
||||
|
||||
CoverProviderSearchResults results;
|
||||
int i = 0;
|
||||
for (const QJsonValue &value_album : array_albums) {
|
||||
for (const auto &value_included : array_included) {
|
||||
|
||||
if (!value_album.isObject()) {
|
||||
qLog(Debug) << "OpenTidal: Invalid Json reply: Albums array value is not a object.";
|
||||
if (!value_included.isObject()) {
|
||||
qLog(Error) << "OpenTidal: Invalid Json reply: Albums array value is not a object.";
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_album = value_album.toObject();
|
||||
const QJsonObject object_included = value_included.toObject();
|
||||
|
||||
if (!obj_album.contains("resource"_L1) || !obj_album["resource"_L1].isObject()) {
|
||||
qLog(Debug) << "OpenTidal: Invalid Json reply: Albums array album is missing resource object.";
|
||||
if (!object_included.contains("attributes"_L1) || !object_included["attributes"_L1].isObject()) {
|
||||
qLog(Error) << "OpenTidal: Invalid Json reply: Included array item is missing attributes object." << object_included;
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_resource = obj_album["resource"_L1].toObject();
|
||||
const QJsonObject object_attributes = object_included["attributes"_L1].toObject();
|
||||
|
||||
if (!obj_resource.contains("artists"_L1) || !obj_resource["artists"_L1].isArray()) {
|
||||
qLog(Debug) << "OpenTidal: Invalid Json reply: Resource is missing artists array.";
|
||||
if (!object_attributes.contains("title"_L1) || !object_attributes["title"_L1].isString()) {
|
||||
qLog(Error) << "OpenTidal: Invalid Json reply: Attributes is missing title string." << object_attributes;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!obj_resource.contains("title"_L1) || !obj_resource["title"_L1].isString()) {
|
||||
qLog(Debug) << "OpenTidal: Invalid Json reply: Resource is missing title.";
|
||||
if (!object_attributes.contains("imageLinks"_L1) || !object_attributes["imageLinks"_L1].isArray()) {
|
||||
qLog(Error) << "OpenTidal: Invalid Json reply: Attributes is missing imageLinks object." << object_attributes;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!obj_resource.contains("imageCover"_L1) || !obj_resource["imageCover"_L1].isArray()) {
|
||||
qLog(Debug) << "OpenTidal: Invalid Json reply: Resource is missing imageCover array.";
|
||||
continue;
|
||||
}
|
||||
const QString album = object_attributes["title"_L1].toString();
|
||||
const QJsonArray array_imagelinks = object_attributes["imageLinks"_L1].toArray();
|
||||
|
||||
QString artist;
|
||||
const QString album = obj_resource["title"_L1].toString();
|
||||
|
||||
const QJsonArray array_artists = obj_resource["artists"_L1].toArray();
|
||||
for (const QJsonValue &value_artist : array_artists) {
|
||||
if (!value_artist.isObject()) {
|
||||
for (const auto &value_imagelink : array_imagelinks) {
|
||||
if (!value_imagelink.isObject()) {
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_artist = value_artist.toObject();
|
||||
if (!obj_artist.contains("name"_L1)) {
|
||||
const QJsonObject object_imagelink = value_imagelink.toObject();
|
||||
if (!object_imagelink.contains("href"_L1) || !object_imagelink.contains("meta"_L1) || !object_imagelink["meta"_L1].isObject()) {
|
||||
continue;
|
||||
}
|
||||
artist = obj_artist["name"_L1].toString();
|
||||
break;
|
||||
}
|
||||
|
||||
const QJsonArray array_covers = obj_resource["imageCover"_L1].toArray();
|
||||
for (const QJsonValue &value_cover : array_covers) {
|
||||
if (!value_cover.isObject()) {
|
||||
QJsonObject object_meta = object_imagelink["meta"_L1].toObject();
|
||||
if (!object_meta.contains("width"_L1) || !object_meta.contains("height"_L1)) {
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_cover = value_cover.toObject();
|
||||
if (!obj_cover.contains("url"_L1) || !obj_cover.contains("width"_L1) || !obj_cover.contains("height"_L1)) {
|
||||
continue;
|
||||
}
|
||||
const QUrl url(obj_cover["url"_L1].toString());
|
||||
const int width = obj_cover["width"_L1].toInt();
|
||||
const int height = obj_cover["height"_L1].toInt();
|
||||
const QUrl url(object_imagelink["href"_L1].toString());
|
||||
const int width = object_meta["width"_L1].toInt();
|
||||
const int height = object_meta["height"_L1].toInt();
|
||||
if (!url.isValid()) continue;
|
||||
if (width < 640 || height < 640) continue;
|
||||
CoverProviderSearchResult cover_result;
|
||||
cover_result.artist = artist;
|
||||
cover_result.artist = search_request->artist;
|
||||
cover_result.album = Song::AlbumRemoveDiscMisc(album);
|
||||
cover_result.image_url = url;
|
||||
cover_result.image_size = QSize(width, height);
|
||||
@@ -448,8 +336,6 @@ void OpenTidalCoverProvider::HandleSearchReply(QNetworkReply *reply, SearchReque
|
||||
}
|
||||
}
|
||||
|
||||
Q_EMIT SearchFinished(search_request->id, results);
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::FinishAllSearches() {
|
||||
@@ -465,7 +351,7 @@ void OpenTidalCoverProvider::FinishAllSearches() {
|
||||
|
||||
void OpenTidalCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << "Tidal:" << error;
|
||||
qLog(Error) << "OpenTidal:" << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2024-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,15 +22,10 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QQueue>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include <QSslError>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
@@ -38,13 +33,13 @@
|
||||
class QNetworkReply;
|
||||
class NetworkAccessManager;
|
||||
class QTimer;
|
||||
class OAuthenticator;
|
||||
|
||||
class OpenTidalCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OpenTidalCoverProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||
~OpenTidalCoverProvider() override;
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override;
|
||||
void CancelSearch(const int id) override;
|
||||
@@ -60,33 +55,24 @@ class OpenTidalCoverProvider : public JsonCoverProvider {
|
||||
using SearchRequestPtr = SharedPtr<SearchRequest>;
|
||||
|
||||
private:
|
||||
void LoadSession();
|
||||
void LoginCheck();
|
||||
void Login();
|
||||
QJsonObject GetJsonObject(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void SendSearchRequest(SearchRequestPtr request);
|
||||
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
|
||||
void FinishAllSearches();
|
||||
void Error(const QString &error, const QVariant &debug = QVariant()) override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void OAuthFinished(const bool success, const QString &error = QString());
|
||||
void FlushRequests();
|
||||
void LoginFinished(QNetworkReply *reply);
|
||||
void HandleLoginSSLErrors(const QList<QSslError> &ssl_errors);
|
||||
void HandleSearchReply(QNetworkReply *reply, OpenTidalCoverProvider::SearchRequestPtr search_request);
|
||||
|
||||
private:
|
||||
QTimer *login_timer_;
|
||||
OAuthenticator *oauth_;
|
||||
QTimer *timer_flush_requests_;
|
||||
bool login_in_progress_;
|
||||
QDateTime last_login_attempt_;
|
||||
bool have_login_;
|
||||
QString token_type_;
|
||||
QString access_token_;
|
||||
qint64 login_time_;
|
||||
qint64 expires_in_;
|
||||
QQueue<SearchRequestPtr> search_requests_queue_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
};
|
||||
|
||||
#endif // OPENTIDALCOVERPROVIDER_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
|
||||
@@ -22,8 +22,6 @@
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
@@ -35,6 +33,7 @@
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
@@ -49,26 +48,27 @@ using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
constexpr int kLimit = 10;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QobuzCoverProvider::QobuzCoverProvider(const QobuzServicePtr service, SharedPtr<NetworkAccessManager> network, QObject *parent)
|
||||
: JsonCoverProvider(u"Qobuz"_s, true, true, 2.0, true, true, network, parent),
|
||||
service_(service) {}
|
||||
|
||||
QobuzCoverProvider::~QobuzCoverProvider() {
|
||||
bool QobuzCoverProvider::authenticated() const {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
return service_->authenticated();
|
||||
|
||||
}
|
||||
|
||||
void QobuzCoverProvider::ClearSession() {
|
||||
|
||||
service_->ClearSession();
|
||||
|
||||
}
|
||||
|
||||
bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album, const QString &title, const int id) {
|
||||
|
||||
if (artist.isEmpty() && album.isEmpty() && title.isEmpty()) return false;
|
||||
if (!service_->authenticated() || (artist.isEmpty() && album.isEmpty() && title.isEmpty())) return false;
|
||||
|
||||
QString resource;
|
||||
QString query = artist;
|
||||
@@ -99,12 +99,12 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album
|
||||
QUrl url(QLatin1String(QobuzService::kApiUrl) + QLatin1Char('/') + resource);
|
||||
url.setQuery(url_query);
|
||||
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
|
||||
req.setRawHeader("X-App-Id", service_->app_id().toUtf8());
|
||||
req.setRawHeader("X-User-Auth-Token", service_->user_auth_token().toUtf8());
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
QNetworkRequest network_request(url);
|
||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
|
||||
network_request.setRawHeader("X-App-Id", service_->app_id().toUtf8());
|
||||
network_request.setRawHeader("X-User-Auth-Token", service_->user_auth_token().toUtf8());
|
||||
QNetworkReply *reply = network_->get(network_request);
|
||||
replies_ << reply;
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); });
|
||||
|
||||
@@ -114,46 +114,56 @@ bool QobuzCoverProvider::StartSearch(const QString &artist, const QString &album
|
||||
|
||||
void QobuzCoverProvider::CancelSearch(const int id) { Q_UNUSED(id); }
|
||||
|
||||
QByteArray QobuzCoverProvider::GetReplyData(QNetworkReply *reply) {
|
||||
JsonBaseRequest::JsonObjectResult QobuzCoverProvider::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.
|
||||
Error(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("code"_L1) && json_object.contains("status"_L1) && json_object.contains("message"_L1)) {
|
||||
const int code = json_object["code"_L1].toInt();
|
||||
const QString message = json_object["message"_L1].toString();
|
||||
result.error_code = ErrorCode::APIError;
|
||||
result.error_message = QStringLiteral("%1 (%2)").arg(message).arg(code);
|
||||
}
|
||||
else {
|
||||
result.json_object = json_document.object();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// See if there is Json data containing "status", "code" and "message" - then use that instead.
|
||||
data = reply->readAll();
|
||||
QString error;
|
||||
QJsonParseError parse_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
|
||||
if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (!json_obj.isEmpty() && json_obj.contains("status"_L1) && json_obj.contains("code"_L1) && json_obj.contains("message"_L1)) {
|
||||
int code = json_obj["code"_L1].toInt();
|
||||
QString message = json_obj["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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||
service_->ClearSession();
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
@@ -165,49 +175,45 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
reply->deleteLater();
|
||||
|
||||
CoverProviderSearchResults results;
|
||||
const QScopeGuard search_finished = 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_type;
|
||||
if (json_obj.contains("albums"_L1)) {
|
||||
value_type = json_obj["albums"_L1];
|
||||
if (json_object.contains("albums"_L1)) {
|
||||
value_type = json_object["albums"_L1];
|
||||
}
|
||||
else if (json_obj.contains("tracks"_L1)) {
|
||||
value_type = json_obj["tracks"_L1];
|
||||
else if (json_object.contains("tracks"_L1)) {
|
||||
value_type = json_object["tracks"_L1];
|
||||
}
|
||||
else {
|
||||
Error(u"Json reply is missing albums and tracks object."_s, json_obj);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
Error(u"Json reply is missing albums and tracks object."_s, json_object);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value_type.isObject()) {
|
||||
Error(u"Json albums or tracks is not a object."_s, value_type);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QJsonObject obj_type = value_type.toObject();
|
||||
const QJsonObject object_type = value_type.toObject();
|
||||
|
||||
if (!obj_type.contains("items"_L1)) {
|
||||
Error(u"Json albums or tracks object does not contain items."_s, obj_type);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
if (!object_type.contains("items"_L1)) {
|
||||
Error(u"Json albums or tracks object does not contain items."_s, object_type);
|
||||
return;
|
||||
}
|
||||
QJsonValue value_items = obj_type["items"_L1];
|
||||
const QJsonValue value_items = object_type["items"_L1];
|
||||
|
||||
if (!value_items.isArray()) {
|
||||
Error(u"Json albums or track object items is not a array."_s, value_items);
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
const QJsonArray array_items = value_items.toArray();
|
||||
@@ -218,52 +224,52 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
Error(u"Invalid Json reply, value in items is not a object."_s);
|
||||
continue;
|
||||
}
|
||||
QJsonObject item_obj = value.toObject();
|
||||
const QJsonObject item_object = value.toObject();
|
||||
|
||||
QJsonObject obj_album;
|
||||
if (item_obj.contains("album"_L1)) {
|
||||
if (!item_obj["album"_L1].isObject()) {
|
||||
Error(u"Invalid Json reply, items album is not a object."_s, item_obj);
|
||||
QJsonObject object_album;
|
||||
if (item_object.contains("album"_L1)) {
|
||||
if (!item_object["album"_L1].isObject()) {
|
||||
Error(u"Invalid Json reply, items album is not a object."_s, item_object);
|
||||
continue;
|
||||
}
|
||||
obj_album = item_obj["album"_L1].toObject();
|
||||
object_album = item_object["album"_L1].toObject();
|
||||
}
|
||||
else {
|
||||
obj_album = item_obj;
|
||||
object_album = item_object;
|
||||
}
|
||||
|
||||
if (!obj_album.contains("artist"_L1) || !obj_album.contains("image"_L1) || !obj_album.contains("title"_L1)) {
|
||||
Error(u"Invalid Json reply, item is missing artist, title or image."_s, obj_album);
|
||||
if (!object_album.contains("artist"_L1) || !object_album.contains("image"_L1) || !object_album.contains("title"_L1)) {
|
||||
Error(u"Invalid Json reply, item is missing artist, title or image."_s, object_album);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString album = obj_album["title"_L1].toString();
|
||||
const QString album = object_album["title"_L1].toString();
|
||||
|
||||
// Artist
|
||||
QJsonValue value_artist = obj_album["artist"_L1];
|
||||
const QJsonValue value_artist = object_album["artist"_L1];
|
||||
if (!value_artist.isObject()) {
|
||||
Error(u"Invalid Json reply, items (album) artist is not a object."_s, value_artist);
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_artist = value_artist.toObject();
|
||||
if (!obj_artist.contains("name"_L1)) {
|
||||
Error(u"Invalid Json reply, items (album) artist is missing name."_s, obj_artist);
|
||||
const QJsonObject object_artist = value_artist.toObject();
|
||||
if (!object_artist.contains("name"_L1)) {
|
||||
Error(u"Invalid Json reply, items (album) artist is missing name."_s, object_artist);
|
||||
continue;
|
||||
}
|
||||
QString artist = obj_artist["name"_L1].toString();
|
||||
const QString artist = object_artist["name"_L1].toString();
|
||||
|
||||
// Image
|
||||
QJsonValue value_image = obj_album["image"_L1];
|
||||
if (!value_image.isObject()) {
|
||||
Error(u"Invalid Json reply, items (album) image is not a object."_s, value_image);
|
||||
const QJsonValue _timer_ = object_album["image"_L1];
|
||||
if (!_timer_.isObject()) {
|
||||
Error(u"Invalid Json reply, items (album) image is not a object."_s, _timer_);
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj_image = value_image.toObject();
|
||||
if (!obj_image.contains("large"_L1)) {
|
||||
Error(u"Invalid Json reply, items (album) image is missing large."_s, obj_image);
|
||||
const QJsonObject object_image = _timer_.toObject();
|
||||
if (!object_image.contains("large"_L1)) {
|
||||
Error(u"Invalid Json reply, items (album) image is missing large."_s, object_image);
|
||||
continue;
|
||||
}
|
||||
QUrl cover_url(obj_image["large"_L1].toString());
|
||||
const QUrl cover_url(object_image["large"_L1].toString());
|
||||
|
||||
CoverProviderSearchResult cover_result;
|
||||
cover_result.artist = artist;
|
||||
@@ -273,7 +279,6 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
results << cover_result;
|
||||
|
||||
}
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -22,44 +22,37 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QSslError>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
#include "qobuz/qobuzservice.h"
|
||||
|
||||
class QNetworkReply;
|
||||
class NetworkAccessManager;
|
||||
class QobuzService;
|
||||
|
||||
class QobuzCoverProvider : public JsonCoverProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QobuzCoverProvider(const QobuzServicePtr service, SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||
~QobuzCoverProvider() override;
|
||||
explicit QobuzCoverProvider(const SharedPtr<QobuzService> service, SharedPtr<NetworkAccessManager> network, QObject *parent = nullptr);
|
||||
|
||||
virtual bool authenticated() const override;
|
||||
|
||||
bool StartSearch(const QString &artist, const QString &album, const QString &title, const int id) override;
|
||||
void CancelSearch(const int id) override;
|
||||
|
||||
bool IsAuthenticated() const override { return service_ && service_->authenticated(); }
|
||||
void Deauthenticate() override { if (service_) service_->Logout(); }
|
||||
void ClearSession() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id);
|
||||
|
||||
private:
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant()) override;
|
||||
|
||||
private:
|
||||
QobuzServicePtr service_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
SharedPtr<QobuzService> service_;
|
||||
};
|
||||
|
||||
#endif // QOBUZCOVERPROVIDER_H
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user