Compare commits
194 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ce0ed2ef8 | ||
|
|
176768f7f8 | ||
|
|
50b954034c | ||
|
|
cab7b6c335 | ||
|
|
fce1dacafc | ||
|
|
94aa6fb940 | ||
|
|
0cfd4aaad1 | ||
|
|
9e72b4fe80 | ||
|
|
1151443372 | ||
|
|
8f6993e7c8 | ||
|
|
a6ab1a7689 | ||
|
|
098b21d818 | ||
|
|
d61adeb595 | ||
|
|
8bfc3bc41c | ||
|
|
0dda2feec3 | ||
|
|
1d0d03ed83 | ||
|
|
330284f03e | ||
|
|
fc3ed3a2ce | ||
|
|
6a656036fe | ||
|
|
5c76c633a5 | ||
|
|
d487c3ea07 | ||
|
|
83dca405af | ||
|
|
acd0b6d3ea | ||
|
|
159242aff4 | ||
|
|
4b014253cf | ||
|
|
1ec6b5582e | ||
|
|
08b8d04500 | ||
|
|
54679b1d57 | ||
|
|
8d648e668e | ||
|
|
5897e786dc | ||
|
|
7f549aa991 | ||
|
|
792e7b6274 | ||
|
|
92c58b0b60 | ||
|
|
5fac9a1c8d | ||
|
|
7f4f715003 | ||
|
|
75d1d4098e | ||
|
|
30e80068a3 | ||
|
|
5fe9a1528f | ||
|
|
7777eda115 | ||
|
|
ce4f2ece93 | ||
|
|
52399d73fe | ||
|
|
6e98166148 | ||
|
|
c658a77b05 | ||
|
|
1880dc8153 | ||
|
|
b5fd3d5717 | ||
|
|
3c3480fb84 | ||
|
|
f628914173 | ||
|
|
c100fb1bb8 | ||
|
|
8c804c4fba | ||
|
|
912a7c7da9 | ||
|
|
0a5815c82e | ||
|
|
6513b3032b | ||
|
|
8c51401bdc | ||
|
|
45fc9c83d4 | ||
|
|
be57d8147a | ||
|
|
a97908fb6b | ||
|
|
c0417d4bb3 | ||
|
|
062e2cfb84 | ||
|
|
700f7dbe36 | ||
|
|
0487118dad | ||
|
|
f3d088e48b | ||
|
|
f8afd49fcf | ||
|
|
363fcb5aba | ||
|
|
183aba4181 | ||
|
|
742be01aa6 | ||
|
|
38c8054873 | ||
|
|
da9e9840b8 | ||
|
|
c4646531b0 | ||
|
|
65d9b6a9e9 | ||
|
|
046f40fbca | ||
|
|
a0ca50ac30 | ||
|
|
d939733675 | ||
|
|
61a8a3a84a | ||
|
|
d4858a338c | ||
|
|
31380a5bd4 | ||
|
|
e45b9aabeb | ||
|
|
27e782d8cf | ||
|
|
0bfc2ee198 | ||
|
|
e7fc4b1706 | ||
|
|
6dea1a2149 | ||
|
|
7844a2b932 | ||
|
|
96a53bfbe5 | ||
|
|
fe5fbae4b4 | ||
|
|
a9140232e5 | ||
|
|
835090dd96 | ||
|
|
af5590dcb1 | ||
|
|
26b5588d7d | ||
|
|
390bf049f2 | ||
|
|
321272b695 | ||
|
|
342805e0f3 | ||
|
|
e614626913 | ||
|
|
2ddacf2f98 | ||
|
|
a47531d4ce | ||
|
|
84b758e395 | ||
|
|
51b69a85c4 | ||
|
|
52774a3222 | ||
|
|
9030b2567b | ||
|
|
ee7bb449a5 | ||
|
|
d901258f11 | ||
|
|
6372c5ee7d | ||
|
|
75f0402793 | ||
|
|
20e5c014ef | ||
|
|
1ebc32c3aa | ||
|
|
a5f94b608b | ||
|
|
e0d61223a4 | ||
|
|
459eea5bc4 | ||
|
|
09d02c53a3 | ||
|
|
61a701554e | ||
|
|
d280e6426f | ||
|
|
5b9bb3efa7 | ||
|
|
b8cbe49f8c | ||
|
|
633e5707ef | ||
|
|
d54290c3a7 | ||
|
|
3ef2b53e46 | ||
|
|
d3a4dd6da6 | ||
|
|
0158f7f08a | ||
|
|
8cea020fac | ||
|
|
f6b38fecb0 | ||
|
|
5e2729fafe | ||
|
|
19dce1c25d | ||
|
|
00bb722e25 | ||
|
|
cbaf4d3121 | ||
|
|
4b5370044b | ||
|
|
ffbe1ec9fd | ||
|
|
53e43db91b | ||
|
|
2858cdabc2 | ||
|
|
cf74eeb120 | ||
|
|
790a1b4dbf | ||
|
|
ee6332af1e | ||
|
|
bf0704f6b2 | ||
|
|
ae13fe7f52 | ||
|
|
90678e72ac | ||
|
|
a0ec244008 | ||
|
|
fba4f84fb6 | ||
|
|
950774f1c8 | ||
|
|
340bc21537 | ||
|
|
a86ba4dffc | ||
|
|
d6bc6e33c0 | ||
|
|
7e128a9af5 | ||
|
|
0f0746be9d | ||
|
|
bec3fe9fd5 | ||
|
|
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 |
159
.github/workflows/build.yml
vendored
159
.github/workflows/build.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
opensuse_version: [ 'tumbleweed', 'leap:15.6' ]
|
||||
opensuse_version: [ 'tumbleweed', 'leap:15.6', 'leap:16.0' ]
|
||||
container:
|
||||
image: opensuse/${{matrix.opensuse_version}}
|
||||
steps:
|
||||
@@ -27,11 +27,11 @@ jobs:
|
||||
- name: Upgrade packages (Leap)
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
run: zypper -n --gpg-auto-import-keys up
|
||||
- name: Install gcc (Tumbleweed)
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
- name: Install gcc
|
||||
if: matrix.opensuse_version != 'leap:15.6'
|
||||
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
|
||||
- name: Install gcc (Leap)
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
- name: Install gcc (leap:15.6)
|
||||
if: matrix.opensuse_version == 'leap:15.6'
|
||||
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
|
||||
- name: Install packages
|
||||
run: >
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
- name: Install kdsingleapplication-qt6-devel
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
if: matrix.opensuse_version != 'leap:15.6'
|
||||
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.opensuse_version == 'leap:15.6'
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -115,14 +115,14 @@ jobs:
|
||||
- name: Copy source tarball
|
||||
working-directory: build
|
||||
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
|
||||
- name: Build RPM (Tumbleweed)
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
- name: Build RPM
|
||||
if: matrix.opensuse_version != 'leap:15.6'
|
||||
env:
|
||||
RPM_BUILD_NCPUS: 4
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
- name: Build RPM (Leap)
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
- name: Build RPM (leap:15.6)
|
||||
if: matrix.opensuse_version == 'leap:15.6'
|
||||
env:
|
||||
RPM_BUILD_NCPUS: 4
|
||||
CC: gcc-14
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
fedora_version: [ '39', '40', '41', '42' ]
|
||||
fedora_version: [ '41', '42', '43' ]
|
||||
container:
|
||||
image: fedora:${{matrix.fedora_version}}
|
||||
steps:
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
- name: Remove files
|
||||
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -409,7 +409,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -449,7 +449,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
debian_version: [ 'bookworm', 'trixie' ]
|
||||
debian_version: [ 'bookworm', 'trixie', 'forky' ]
|
||||
container:
|
||||
image: debian:${{matrix.debian_version}}
|
||||
steps:
|
||||
@@ -499,7 +499,11 @@ jobs:
|
||||
qt6-tools-dev-tools
|
||||
qt6-l10n-tools
|
||||
rapidjson-dev
|
||||
- name: Install KDSingleApplication
|
||||
if: matrix.debian_version != 'bookworm'
|
||||
run: apt install -y libkdsingleapplication-qt6-dev
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.debian_version == 'bookworm'
|
||||
run: |
|
||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||
cd KDSingleApplication
|
||||
@@ -507,7 +511,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -538,7 +542,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'oracular' ]
|
||||
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -591,7 +595,11 @@ jobs:
|
||||
qt6-tools-dev-tools
|
||||
qt6-l10n-tools
|
||||
rapidjson-dev
|
||||
- name: Install KDSingleApplication
|
||||
if: matrix.ubuntu_version != 'noble' && matrix.ubuntu_version != 'plucky'
|
||||
run: apt install -y libkdsingleapplication-qt6-dev
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.ubuntu_version == 'noble' || matrix.ubuntu_version == 'plucky'
|
||||
run: |
|
||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||
cd KDSingleApplication
|
||||
@@ -599,7 +607,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -631,7 +639,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'oracular' ]
|
||||
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -691,7 +699,7 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y keyboxd
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -727,13 +735,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Build FreeBSD
|
||||
id: build-freebsd
|
||||
uses: vmactions/freebsd-vm@v1.1.9
|
||||
uses: vmactions/freebsd-vm@v1.2.4
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
@@ -752,13 +760,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Build OpenBSD
|
||||
id: build-openbsd
|
||||
uses: vmactions/openbsd-vm@v1.1.6
|
||||
uses: vmactions/openbsd-vm@v1.2.1
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
@@ -788,7 +796,7 @@ jobs:
|
||||
|
||||
- name: Set MACOSX_DEPLOYMENT_TARGET
|
||||
run: |
|
||||
for i in 13 14 15; do
|
||||
for i in 12 13 14 15; do
|
||||
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
|
||||
echo "Using macOS SDK ${i}"
|
||||
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
|
||||
@@ -818,7 +826,7 @@ jobs:
|
||||
rm -f uninstall.sh
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -946,7 +954,7 @@ jobs:
|
||||
|
||||
- name: Set MACOSX_DEPLOYMENT_TARGET
|
||||
run: |
|
||||
for i in 13 14 15; do
|
||||
for i in 12 13 14 15; do
|
||||
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
|
||||
echo "Using macOS SDK ${i}"
|
||||
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
|
||||
@@ -969,7 +977,7 @@ jobs:
|
||||
run: echo "cmake_buildtype=$(echo ${{env.buildtype}} | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}')" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1072,7 +1080,7 @@ jobs:
|
||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1246,12 +1254,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
|
||||
@@ -1265,10 +1303,28 @@ jobs:
|
||||
shell: bash
|
||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
||||
|
||||
- name: Show SDK versions
|
||||
shell: bash
|
||||
run: ls -la "c:/Program Files (x86)/Windows Kits/10/include"
|
||||
|
||||
- name: Set SDK version
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
run: echo "sdk_version=10.0.19041.0" >> $GITHUB_ENV
|
||||
|
||||
- name: Set SDK version
|
||||
if: matrix.arch == 'arm64'
|
||||
shell: bash
|
||||
run: echo "sdk_version=10.0.26100.0" >> $GITHUB_ENV
|
||||
|
||||
- name: Install rsync
|
||||
shell: cmd
|
||||
run: choco install --no-progress rsync
|
||||
|
||||
- name: Set SSH command
|
||||
shell: bash
|
||||
run: echo "ssh_command=/c/ProgramData/chocolatey/lib/rsync/tools/$(ls -1 /c/ProgramData/chocolatey/lib/rsync/tools/ | grep -v '\.zip$' | grep '^cwrsync' | tail -1)/bin/ssh.exe" >> $GITHUB_ENV
|
||||
|
||||
- name: Cleanup PATH
|
||||
uses: egor-tensin/cleanup-path@v4
|
||||
with:
|
||||
@@ -1293,7 +1349,9 @@ jobs:
|
||||
|
||||
- name: Copy bin files
|
||||
shell: bash
|
||||
run: cp /c/strawberry/c/bin/{patch.exe,strip.exe,strings.exe,objdump.exe} ${{env.prefix_path_unix}}/bin
|
||||
run: |
|
||||
cp /c/mingw64/bin/{strip.exe,strings.exe,objdump.exe} ${{env.prefix_path_unix}}/bin
|
||||
cp /c/strawberry/c/bin/patch.exe ${{env.prefix_path_unix}}/bin
|
||||
|
||||
- name: Delete conflicting files
|
||||
shell: bash
|
||||
@@ -1347,11 +1405,11 @@ jobs:
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: ${{matrix.arch}}
|
||||
sdk: 10.0.20348.0
|
||||
sdk: ${{env.sdk_version}}
|
||||
vsversion: 2022
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1364,15 +1422,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
|
||||
@@ -1384,14 +1445,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
|
||||
@@ -1562,7 +1623,7 @@ jobs:
|
||||
- name: rsync
|
||||
if: steps.set-upload-path.outputs.upload_path != ''
|
||||
shell: bash
|
||||
run: rsync -e "/c/ProgramData/chocolatey/lib/rsync/tools/bin/ssh.exe -p ${{secrets.SSH_PORT}} -o StrictHostKeyChecking=no" -var build/StrawberrySetup*.exe ${{secrets.SSH_USER}}@${{secrets.SSH_HOST}}:${{steps.set-upload-path.outputs.upload_path}}/
|
||||
run: rsync -e "${{env.ssh_command}} -p ${{secrets.SSH_PORT}} -o StrictHostKeyChecking=no" -var build/StrawberrySetup*.exe ${{secrets.SSH_USER}}@${{secrets.SSH_HOST}}:${{steps.set-upload-path.outputs.upload_path}}/
|
||||
|
||||
|
||||
upload:
|
||||
@@ -1581,11 +1642,11 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: sudo apt install -y git rsync
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: artifacts
|
||||
- name: SSH Setup
|
||||
@@ -1629,7 +1690,7 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: sudo apt install -y git jq gh
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Show release assets
|
||||
@@ -1637,7 +1698,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Add artifacts to release
|
||||
|
||||
42
3rdparty/discord-rpc/CMakeLists.txt
vendored
42
3rdparty/discord-rpc/CMakeLists.txt
vendored
@@ -1 +1,41 @@
|
||||
add_subdirectory(src)
|
||||
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})
|
||||
|
||||
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
|
||||
@@ -1,4 +1,27 @@
|
||||
#include "connection.h"
|
||||
/*
|
||||
* 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>
|
||||
@@ -28,28 +51,34 @@ static int MsgFlags = 0;
|
||||
#endif
|
||||
|
||||
static const char *GetTempPath() {
|
||||
|
||||
const char *temp = getenv("XDG_RUNTIME_DIR");
|
||||
temp = temp ? temp : getenv("TMPDIR");
|
||||
temp = temp ? temp : getenv("TMP");
|
||||
temp = temp ? temp : getenv("TEMP");
|
||||
temp = temp ? temp : "/tmp";
|
||||
|
||||
return temp;
|
||||
|
||||
}
|
||||
|
||||
/*static*/ BaseConnection *BaseConnection::Create() {
|
||||
BaseConnection *BaseConnection::Create() {
|
||||
PipeAddr.sun_family = AF_UNIX;
|
||||
return &Connection;
|
||||
}
|
||||
|
||||
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(c);
|
||||
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);
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
@@ -61,8 +90,7 @@ bool BaseConnection::Open() {
|
||||
#endif
|
||||
|
||||
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
|
||||
snprintf(
|
||||
PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, 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;
|
||||
@@ -70,10 +98,13 @@ bool BaseConnection::Open() {
|
||||
}
|
||||
}
|
||||
self->Close();
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
@@ -81,11 +112,14 @@ bool BaseConnection::Close() {
|
||||
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);
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
@@ -95,11 +129,14 @@ bool BaseConnection::Write(const void *data, size_t length) {
|
||||
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);
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
@@ -115,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
|
||||
else if (res == 0) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return static_cast<size_t>(res) == length;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
} // namespace discord_rpc
|
||||
@@ -1,9 +1,33 @@
|
||||
#include "connection.h"
|
||||
/*
|
||||
* 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>
|
||||
|
||||
@@ -19,24 +43,26 @@ struct BaseConnectionWin : public BaseConnection {
|
||||
|
||||
static BaseConnectionWin Connection;
|
||||
|
||||
/*static*/ BaseConnection *BaseConnection::Create() {
|
||||
BaseConnection *BaseConnection::Create() {
|
||||
return &Connection;
|
||||
}
|
||||
|
||||
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
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);
|
||||
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;
|
||||
@@ -57,17 +83,22 @@ bool BaseConnection::Open() {
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -85,11 +116,13 @@ bool BaseConnection::Write(const void *data, size_t length) {
|
||||
}
|
||||
const DWORD bytesLength = static_cast<DWORD>(length);
|
||||
DWORD bytesWritten = 0;
|
||||
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE &&
|
||||
bytesWritten == bytesLength;
|
||||
|
||||
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;
|
||||
@@ -119,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,26 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
@@ -5,6 +28,7 @@
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#include <cstdio>
|
||||
@@ -46,12 +70,8 @@ static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat,
|
||||
#endif
|
||||
#define RegSetKeyValueW regset
|
||||
|
||||
static LSTATUS regset(HKEY hkey,
|
||||
LPCWSTR subkey,
|
||||
LPCWSTR name,
|
||||
DWORD type,
|
||||
const void *data,
|
||||
DWORD len) {
|
||||
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]) {
|
||||
@@ -64,16 +84,18 @@ static LSTATUS regset(HKEY hkey,
|
||||
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];
|
||||
wchar_t exeFilePath[MAX_PATH]{};
|
||||
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
|
||||
wchar_t openCommand[1024];
|
||||
wchar_t openCommand[1024]{};
|
||||
|
||||
if (command && command[0]) {
|
||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
|
||||
@@ -83,18 +105,16 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
|
||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
|
||||
}
|
||||
|
||||
wchar_t protocolName[64];
|
||||
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 protocolDescription[128]{};
|
||||
StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
|
||||
wchar_t urlProtocol = 0;
|
||||
|
||||
wchar_t keyName[256];
|
||||
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);
|
||||
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;
|
||||
@@ -102,8 +122,7 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
|
||||
DWORD len;
|
||||
LSTATUS result;
|
||||
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
|
||||
result =
|
||||
RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
|
||||
result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing description\n");
|
||||
}
|
||||
@@ -114,26 +133,26 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
|
||||
fprintf(stderr, "Error writing description\n");
|
||||
}
|
||||
|
||||
result = RegSetKeyValueW(
|
||||
key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
|
||||
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));
|
||||
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];
|
||||
|
||||
wchar_t appId[32]{};
|
||||
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
|
||||
|
||||
wchar_t openCommand[1024];
|
||||
wchar_t openCommand[1024]{};
|
||||
const wchar_t *wcommand = nullptr;
|
||||
if (command && command[0]) {
|
||||
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
|
||||
@@ -142,42 +161,6 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
|
||||
}
|
||||
|
||||
Discord_RegisterW(appId, wcommand);
|
||||
}
|
||||
|
||||
extern "C" void Discord_RegisterSteamGame(const char *applicationId,
|
||||
const char *steamId) {
|
||||
wchar_t appId[32];
|
||||
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
|
||||
|
||||
wchar_t wSteamId[32];
|
||||
MultiByteToWideChar(CP_UTF8, 0, steamId, -1, wSteamId, 32);
|
||||
|
||||
HKEY key;
|
||||
auto status = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", 0, KEY_READ, &key);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fprintf(stderr, "Error opening Steam key\n");
|
||||
return;
|
||||
}
|
||||
|
||||
wchar_t steamPath[MAX_PATH];
|
||||
DWORD pathBytes = sizeof(steamPath);
|
||||
status = RegQueryValueExW(key, L"SteamExe", nullptr, nullptr, (BYTE *)steamPath, &pathBytes);
|
||||
RegCloseKey(key);
|
||||
if (status != ERROR_SUCCESS || pathBytes < 1) {
|
||||
fprintf(stderr, "Error reading SteamExe key\n");
|
||||
return;
|
||||
}
|
||||
|
||||
DWORD pathChars = pathBytes / sizeof(wchar_t);
|
||||
for (DWORD i = 0; i < pathChars; ++i) {
|
||||
if (steamPath[i] == L'/') {
|
||||
steamPath[i] = L'\\';
|
||||
}
|
||||
}
|
||||
|
||||
wchar_t command[1024];
|
||||
StringCbPrintfW(command, sizeof(command), L"\"%s\" steam://rungameid/%s", steamPath, wSteamId);
|
||||
|
||||
Discord_RegisterW(appId, command);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,44 @@
|
||||
#include "discord_rpc.h"
|
||||
|
||||
#include "backoff.h"
|
||||
#include "discord_register.h"
|
||||
#include "msg_queue.h"
|
||||
#include "rpc_connection.h"
|
||||
#include "serialization.h"
|
||||
/*
|
||||
* 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>
|
||||
|
||||
namespace discord_rpc {
|
||||
#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 };
|
||||
@@ -67,14 +92,12 @@ 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
|
||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
|
||||
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
||||
static auto NextConnect = std::chrono::system_clock::now();
|
||||
static int Pid { 0 };
|
||||
static int Nonce { 1 };
|
||||
|
||||
static void Discord_UpdateConnection(void);
|
||||
class IoThreadHolder {
|
||||
private:
|
||||
std::atomic_bool keepRunning { true };
|
||||
@@ -108,14 +131,55 @@ class IoThreadHolder {
|
||||
|
||||
~IoThreadHolder() { Stop(); }
|
||||
};
|
||||
|
||||
static IoThreadHolder *IoThread { nullptr };
|
||||
|
||||
static void UpdateReconnectTime() {
|
||||
NextConnect = std::chrono::system_clock::now() +
|
||||
std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
|
||||
|
||||
NextConnect = std::chrono::system_clock::now() + std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
|
||||
|
||||
}
|
||||
|
||||
static void Discord_UpdateConnection(void) {
|
||||
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;
|
||||
}
|
||||
@@ -217,54 +281,18 @@ static void Discord_UpdateConnection(void) {
|
||||
SendQueue.CommitSend();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void SignalIOActivity() {
|
||||
if (IoThread != nullptr) {
|
||||
IoThread->Notify();
|
||||
}
|
||||
}
|
||||
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
|
||||
|
||||
static bool RegisterForEvent(const char *evtName) {
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length =
|
||||
JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool DeregisterForEvent(const char *evtName) {
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length =
|
||||
JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" void Discord_Initialize(const char *applicationId,
|
||||
DiscordEventHandlers *handlers,
|
||||
int autoRegister,
|
||||
const char *optionalSteamId) {
|
||||
IoThread = new (std::nothrow) IoThreadHolder();
|
||||
if (IoThread == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoRegister) {
|
||||
if (optionalSteamId && optionalSteamId[0]) {
|
||||
Discord_RegisterSteamGame(applicationId, optionalSteamId);
|
||||
}
|
||||
else {
|
||||
Discord_Register(applicationId, nullptr);
|
||||
}
|
||||
Discord_Register(applicationId, nullptr);
|
||||
}
|
||||
|
||||
Pid = GetProcessId();
|
||||
@@ -323,9 +351,11 @@ extern "C" void Discord_Initialize(const char *applicationId,
|
||||
};
|
||||
|
||||
IoThread->Start();
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_Shutdown(void) {
|
||||
extern "C" void Discord_Shutdown() {
|
||||
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
@@ -341,16 +371,19 @@ extern "C" void Discord_Shutdown(void) {
|
||||
}
|
||||
|
||||
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);
|
||||
QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
|
||||
UpdatePresence.exchange(true);
|
||||
}
|
||||
|
||||
SignalIOActivity();
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_ClearPresence(void) {
|
||||
@@ -358,20 +391,22 @@ extern "C" void Discord_ClearPresence(void) {
|
||||
}
|
||||
|
||||
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++);
|
||||
qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_RunCallbacks(void) {
|
||||
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.
|
||||
@@ -380,8 +415,8 @@ extern "C" void Discord_RunCallbacks(void) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool wasDisconnected = WasJustDisconnected.exchange(false);
|
||||
bool isConnected = Connection->IsOpen();
|
||||
const bool wasDisconnected = WasJustDisconnected.exchange(false);
|
||||
const bool isConnected = Connection->IsOpen();
|
||||
|
||||
if (isConnected) {
|
||||
// if we are connected, disconnect cb first
|
||||
@@ -394,10 +429,7 @@ extern "C" void Discord_RunCallbacks(void) {
|
||||
if (WasJustConnected.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.ready) {
|
||||
DiscordUser du { connectedUser.userId,
|
||||
connectedUser.username,
|
||||
connectedUser.discriminator,
|
||||
connectedUser.avatar };
|
||||
DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||
Handlers.ready(&du);
|
||||
}
|
||||
}
|
||||
@@ -429,7 +461,7 @@ extern "C" void Discord_RunCallbacks(void) {
|
||||
// maybe show them in one common dialog and/or start fetching the avatars in parallel, and if
|
||||
// not it should be trivial for the implementer to make a queue themselves.
|
||||
while (JoinAskQueue.HavePendingSends()) {
|
||||
auto req = JoinAskQueue.GetNextSendMessage();
|
||||
const auto req = JoinAskQueue.GetNextSendMessage();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinRequest) {
|
||||
@@ -447,9 +479,11 @@ extern "C" void Discord_RunCallbacks(void) {
|
||||
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) { \
|
||||
@@ -472,8 +506,5 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
Handlers = {};
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
94
3rdparty/discord-rpc/discord_rpc.h
vendored
Normal file
94
3rdparty/discord-rpc/discord_rpc.h
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_RPC_H
|
||||
#define DISCORD_RPC_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct DiscordRichPresence {
|
||||
int type;
|
||||
int status_display_type;
|
||||
const char *name; /* max 128 bytes */
|
||||
const char *state; /* max 128 bytes */
|
||||
const char *details; /* max 128 bytes */
|
||||
int64_t startTimestamp;
|
||||
int64_t endTimestamp;
|
||||
const char *largeImageKey; /* max 32 bytes */
|
||||
const char *largeImageText; /* max 128 bytes */
|
||||
const char *smallImageKey; /* max 32 bytes */
|
||||
const char *smallImageText; /* max 128 bytes */
|
||||
const char *partyId; /* max 128 bytes */
|
||||
int partySize;
|
||||
int partyMax;
|
||||
int partyPrivacy;
|
||||
const char *matchSecret; /* max 128 bytes */
|
||||
const char *joinSecret; /* max 128 bytes */
|
||||
const char *spectateSecret; /* max 128 bytes */
|
||||
int8_t instance;
|
||||
} DiscordRichPresence;
|
||||
|
||||
typedef struct DiscordUser {
|
||||
const char *userId;
|
||||
const char *username;
|
||||
const char *discriminator;
|
||||
const char *avatar;
|
||||
} DiscordUser;
|
||||
|
||||
typedef struct DiscordEventHandlers {
|
||||
void (*ready)(const DiscordUser *request);
|
||||
void (*disconnected)(int errorCode, const char *message);
|
||||
void (*errored)(int errorCode, const char *message);
|
||||
void (*joinGame)(const char *joinSecret);
|
||||
void (*spectateGame)(const char *spectateSecret);
|
||||
void (*joinRequest)(const DiscordUser *request);
|
||||
} DiscordEventHandlers;
|
||||
|
||||
#define DISCORD_REPLY_NO 0
|
||||
#define DISCORD_REPLY_YES 1
|
||||
#define DISCORD_REPLY_IGNORE 2
|
||||
#define DISCORD_PARTY_PRIVATE 0
|
||||
#define DISCORD_PARTY_PUBLIC 1
|
||||
|
||||
void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister);
|
||||
void Discord_Shutdown(void);
|
||||
|
||||
// checks for incoming messages, dispatches callbacks
|
||||
void Discord_RunCallbacks(void);
|
||||
|
||||
void Discord_UpdatePresence(const DiscordRichPresence *presence);
|
||||
void Discord_ClearPresence(void);
|
||||
|
||||
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
|
||||
|
||||
void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif // DISCORD_RPC_H
|
||||
@@ -1,24 +1,52 @@
|
||||
#include "rpc_connection.h"
|
||||
#include "serialization.h"
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/*static*/ RpcConnection *RpcConnection::Create(const char *applicationId) {
|
||||
RpcConnection *RpcConnection::Create(const char *applicationId) {
|
||||
|
||||
Instance.connection = BaseConnection::Create();
|
||||
StringCopy(Instance.appId, applicationId);
|
||||
return &Instance;
|
||||
|
||||
}
|
||||
|
||||
/*static*/ void RpcConnection::Destroy(RpcConnection *&c) {
|
||||
void RpcConnection::Destroy(RpcConnection *&c) {
|
||||
|
||||
c->Close();
|
||||
BaseConnection::Destroy(c->connection);
|
||||
c = nullptr;
|
||||
|
||||
}
|
||||
|
||||
void RpcConnection::Open() {
|
||||
|
||||
if (state == State::Connected) {
|
||||
return;
|
||||
}
|
||||
@@ -51,17 +79,21 @@ void RpcConnection::Open() {
|
||||
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);
|
||||
@@ -69,14 +101,17 @@ bool RpcConnection::Write(const void *data, size_t length) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool RpcConnection::Read(JsonDocument &message) {
|
||||
|
||||
if (state != State::Connected && state != State::SentHandshake) {
|
||||
return false;
|
||||
}
|
||||
MessageFrame readFrame;
|
||||
MessageFrame readFrame{};
|
||||
for (;;) {
|
||||
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
|
||||
if (!didRead) {
|
||||
@@ -127,7 +162,7 @@ bool RpcConnection::Read(JsonDocument &message) {
|
||||
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
|
||||
@@ -1,11 +1,35 @@
|
||||
#include "serialization.h"
|
||||
#include "connection.h"
|
||||
/*
|
||||
* 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;
|
||||
@@ -26,6 +50,7 @@ void NumberToString(char *dest, T number) {
|
||||
*dest++ = temp[place];
|
||||
}
|
||||
*dest = 0;
|
||||
|
||||
}
|
||||
|
||||
// it's ever so slightly faster to not have to strlen the key
|
||||
@@ -62,24 +87,25 @@ struct WriteArray {
|
||||
|
||||
template<typename T>
|
||||
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
|
||||
|
||||
if (value && value[0]) {
|
||||
w.Key(k, sizeof(T) - 1);
|
||||
w.String(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void JsonWriteNonce(JsonWriter &writer, int nonce) {
|
||||
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,
|
||||
size_t maxLen,
|
||||
int nonce,
|
||||
int pid,
|
||||
const DiscordRichPresence *presence) {
|
||||
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
@@ -102,6 +128,9 @@ size_t JsonWriteRichPresenceObj(char *dest,
|
||||
if (presence->type >= 0 && presence->type <= 5) {
|
||||
WriteKey(writer, "type");
|
||||
writer.Int(presence->type);
|
||||
|
||||
WriteKey(writer, "status_display_type");
|
||||
writer.Int(presence->status_display_type);
|
||||
}
|
||||
|
||||
WriteOptionalString(writer, "name", presence->name);
|
||||
@@ -168,6 +197,7 @@ size_t JsonWriteRichPresenceObj(char *dest,
|
||||
}
|
||||
|
||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
@@ -179,9 +209,11 @@ size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
@@ -197,9 +229,11 @@ size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const cha
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
@@ -215,9 +249,11 @@ size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const c
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce) {
|
||||
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
@@ -243,7 +279,7 @@ size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int rep
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
#pragma once
|
||||
/*
|
||||
* 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
|
||||
@@ -24,12 +50,7 @@ inline size_t StringCopy(char (&dest)[Len], const char *src) {
|
||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
|
||||
|
||||
// Commands
|
||||
struct DiscordRichPresence;
|
||||
size_t JsonWriteRichPresenceObj(char *dest,
|
||||
size_t maxLen,
|
||||
int nonce,
|
||||
int pid,
|
||||
const DiscordRichPresence *presence);
|
||||
size_t 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);
|
||||
@@ -149,35 +170,44 @@ class JsonDocument : public JsonDocumentBase {
|
||||
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) {
|
||||
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
|
||||
12
3rdparty/discord-rpc/include/discord_register.h
vendored
12
3rdparty/discord-rpc/include/discord_register.h
vendored
@@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void Discord_Register(const char* applicationId, const char* command);
|
||||
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
77
3rdparty/discord-rpc/include/discord_rpc.h
vendored
77
3rdparty/discord-rpc/include/discord_rpc.h
vendored
@@ -1,77 +0,0 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
|
||||
// clang-format on
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct DiscordRichPresence {
|
||||
int type;
|
||||
const char* name; /* max 128 bytes */
|
||||
const char* state; /* max 128 bytes */
|
||||
const char* details; /* max 128 bytes */
|
||||
int64_t startTimestamp;
|
||||
int64_t endTimestamp;
|
||||
const char* largeImageKey; /* max 32 bytes */
|
||||
const char* largeImageText; /* max 128 bytes */
|
||||
const char* smallImageKey; /* max 32 bytes */
|
||||
const char* smallImageText; /* max 128 bytes */
|
||||
const char* partyId; /* max 128 bytes */
|
||||
int partySize;
|
||||
int partyMax;
|
||||
int partyPrivacy;
|
||||
const char* matchSecret; /* max 128 bytes */
|
||||
const char* joinSecret; /* max 128 bytes */
|
||||
const char* spectateSecret; /* max 128 bytes */
|
||||
int8_t instance;
|
||||
} DiscordRichPresence;
|
||||
|
||||
typedef struct DiscordUser {
|
||||
const char* userId;
|
||||
const char* username;
|
||||
const char* discriminator;
|
||||
const char* avatar;
|
||||
} DiscordUser;
|
||||
|
||||
typedef struct DiscordEventHandlers {
|
||||
void (*ready)(const DiscordUser* request);
|
||||
void (*disconnected)(int errorCode, const char* message);
|
||||
void (*errored)(int errorCode, const char* message);
|
||||
void (*joinGame)(const char* joinSecret);
|
||||
void (*spectateGame)(const char* spectateSecret);
|
||||
void (*joinRequest)(const DiscordUser* request);
|
||||
} DiscordEventHandlers;
|
||||
|
||||
#define DISCORD_REPLY_NO 0
|
||||
#define DISCORD_REPLY_YES 1
|
||||
#define DISCORD_REPLY_IGNORE 2
|
||||
#define DISCORD_PARTY_PRIVATE 0
|
||||
#define DISCORD_PARTY_PUBLIC 1
|
||||
|
||||
void Discord_Initialize(const char* applicationId,
|
||||
DiscordEventHandlers* handlers,
|
||||
int autoRegister,
|
||||
const char* optionalSteamId);
|
||||
void Discord_Shutdown(void);
|
||||
|
||||
/* checks for incoming messages, dispatches callbacks */
|
||||
void Discord_RunCallbacks(void);
|
||||
|
||||
void Discord_UpdatePresence(const DiscordRichPresence* presence);
|
||||
void Discord_ClearPresence(void);
|
||||
|
||||
void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply);
|
||||
|
||||
void Discord_UpdateHandlers(DiscordEventHandlers* handlers);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif
|
||||
41
3rdparty/discord-rpc/src/CMakeLists.txt
vendored
41
3rdparty/discord-rpc/src/CMakeLists.txt
vendored
@@ -1,41 +0,0 @@
|
||||
set(DISCORD_RPC_SOURCES
|
||||
../include/discord_rpc.h
|
||||
../include/discord_register.h
|
||||
discord_rpc.cpp
|
||||
rpc_connection.h
|
||||
rpc_connection.cpp
|
||||
serialization.h
|
||||
serialization.cpp
|
||||
connection.h
|
||||
backoff.h
|
||||
msg_queue.h
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
list(APPEND DISCORD_RPC_SOURCES connection_unix.cpp)
|
||||
if(APPLE)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
|
||||
add_definitions(-DDISCORD_OSX)
|
||||
else()
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
|
||||
add_definitions(-DDISCORD_LINUX)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND DISCORD_RPC_SOURCES connection_win.cpp discord_register_win.cpp)
|
||||
add_definitions(-DDISCORD_WINDOWS)
|
||||
endif()
|
||||
|
||||
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||
endif()
|
||||
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
|
||||
44
3rdparty/discord-rpc/src/backoff.h
vendored
44
3rdparty/discord-rpc/src/backoff.h
vendored
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
struct Backoff {
|
||||
int64_t minAmount;
|
||||
int64_t maxAmount;
|
||||
int64_t current;
|
||||
int fails;
|
||||
std::mt19937_64 randGenerator;
|
||||
std::uniform_real_distribution<> randDistribution;
|
||||
|
||||
double rand01() { return randDistribution(randGenerator); }
|
||||
|
||||
Backoff(int64_t min, int64_t max)
|
||||
: minAmount(min)
|
||||
, maxAmount(max)
|
||||
, current(min)
|
||||
, fails(0)
|
||||
, randGenerator(static_cast<uint64_t>(time(0)))
|
||||
{
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
fails = 0;
|
||||
current = minAmount;
|
||||
}
|
||||
|
||||
int64_t nextDelay()
|
||||
{
|
||||
++fails;
|
||||
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
|
||||
current = std::min(current + delay, maxAmount);
|
||||
return current;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
22
3rdparty/discord-rpc/src/connection.h
vendored
22
3rdparty/discord-rpc/src/connection.h
vendored
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// This is to wrap the platform specific kinds of connect/read/write.
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// not really connectiony, but need per-platform
|
||||
int GetProcessId();
|
||||
|
||||
struct BaseConnection {
|
||||
static BaseConnection *Create();
|
||||
static void Destroy(BaseConnection *&);
|
||||
bool isOpen { false };
|
||||
bool Open();
|
||||
bool Close();
|
||||
bool Write(const void *data, size_t length);
|
||||
bool Read(void *data, size_t length);
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
104
3rdparty/discord-rpc/src/discord_register_linux.cpp
vendored
104
3rdparty/discord-rpc/src/discord_register_linux.cpp
vendored
@@ -1,104 +0,0 @@
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_register.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <errno.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
static bool Mkdir(const char *path) {
|
||||
int result = mkdir(path, 0755);
|
||||
if (result == 0) {
|
||||
return true;
|
||||
}
|
||||
if (errno == EEXIST) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// we want to register games so we can run them from Discord client as discord-<appid>://
|
||||
extern "C" void Discord_Register(const char *applicationId, const char *command) {
|
||||
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
|
||||
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
char exePath[1024];
|
||||
if (!command || !command[0]) {
|
||||
ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
|
||||
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
|
||||
return;
|
||||
}
|
||||
exePath[size] = '\0';
|
||||
command = exePath;
|
||||
}
|
||||
|
||||
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
|
||||
"Name=Game %s\n"
|
||||
"Exec=%s %%u\n" // note: it really wants that %u in there
|
||||
"Type=Application\n"
|
||||
"NoDisplay=true\n"
|
||||
"Categories=Discord;Games;\n"
|
||||
"MimeType=x-scheme-handler/discord-%s;\n";
|
||||
char desktopFile[2048];
|
||||
int fileLen = snprintf(
|
||||
desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
|
||||
if (fileLen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char desktopFilename[256];
|
||||
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
|
||||
|
||||
char desktopFilePath[1024];
|
||||
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/share");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/applications");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, desktopFilename);
|
||||
|
||||
FILE *fp = fopen(desktopFilePath, "w");
|
||||
if (fp) {
|
||||
fwrite(desktopFile, 1, fileLen, fp);
|
||||
fclose(fp);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
char xdgMimeCommand[1024];
|
||||
snprintf(xdgMimeCommand,
|
||||
sizeof(xdgMimeCommand),
|
||||
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
|
||||
applicationId,
|
||||
applicationId);
|
||||
if (system(xdgMimeCommand) < 0) {
|
||||
fprintf(stderr, "Failed to register mime handler\n");
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void Discord_RegisterSteamGame(const char *applicationId,
|
||||
const char *steamId) {
|
||||
char command[256];
|
||||
sprintf(command, "xdg-open steam://rungameid/%s", steamId);
|
||||
Discord_Register(applicationId, command);
|
||||
}
|
||||
|
||||
80
3rdparty/discord-rpc/src/discord_register_osx.m
vendored
80
3rdparty/discord-rpc/src/discord_register_osx.m
vendored
@@ -1,80 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#include "discord_register.h"
|
||||
|
||||
static void RegisterCommand(const char* applicationId, const char* command)
|
||||
{
|
||||
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
|
||||
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
|
||||
// the command therein (will pass to js's window.open, so requires a url-like thing)
|
||||
|
||||
// Note: will not work for sandboxed apps
|
||||
NSString *home = NSHomeDirectory();
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
|
||||
stringByAppendingPathComponent:@"Application Support"]
|
||||
stringByAppendingPathComponent:@"discord"]
|
||||
stringByAppendingPathComponent:@"games"]
|
||||
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
|
||||
stringByAppendingPathExtension:@"json"];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
|
||||
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
|
||||
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
|
||||
}
|
||||
|
||||
static void RegisterURL(const char* applicationId)
|
||||
{
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "discord-%s", applicationId);
|
||||
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
|
||||
|
||||
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
|
||||
if (!myBundleId) {
|
||||
fprintf(stderr, "No bundle id found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
|
||||
if (!myURL) {
|
||||
fprintf(stderr, "No bundle url found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
|
||||
}
|
||||
}
|
||||
|
||||
void Discord_Register(const char* applicationId, const char* command)
|
||||
{
|
||||
if (command) {
|
||||
RegisterCommand(applicationId, command);
|
||||
}
|
||||
else {
|
||||
// raii lite
|
||||
@autoreleasepool {
|
||||
RegisterURL(applicationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
|
||||
{
|
||||
char command[256];
|
||||
snprintf(command, 256, "steam://rungameid/%s", steamId);
|
||||
Discord_Register(applicationId, command);
|
||||
}
|
||||
40
3rdparty/discord-rpc/src/msg_queue.h
vendored
40
3rdparty/discord-rpc/src/msg_queue.h
vendored
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
|
||||
// a consumer. Mutex up as needed.
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
template <typename ElementType, std::size_t QueueSize>
|
||||
class MsgQueue {
|
||||
ElementType queue_[QueueSize];
|
||||
std::atomic_uint nextAdd_{0};
|
||||
std::atomic_uint nextSend_{0};
|
||||
std::atomic_uint pendingSends_{0};
|
||||
|
||||
public:
|
||||
MsgQueue() {}
|
||||
|
||||
ElementType* GetNextAddMessage()
|
||||
{
|
||||
// if we are falling behind, bail
|
||||
if (pendingSends_.load() >= QueueSize) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = (nextAdd_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitAdd() { ++pendingSends_; }
|
||||
|
||||
bool HavePendingSends() const { return pendingSends_.load() != 0; }
|
||||
ElementType* GetNextSendMessage()
|
||||
{
|
||||
auto index = (nextSend_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitSend() { --pendingSends_; }
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
64
3rdparty/discord-rpc/src/rpc_connection.h
vendored
64
3rdparty/discord-rpc/src/rpc_connection.h
vendored
@@ -1,64 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "connection.h"
|
||||
#include "serialization.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much
|
||||
// smaller.
|
||||
constexpr size_t MaxRpcFrameSize = 64 * 1024;
|
||||
|
||||
struct RpcConnection {
|
||||
enum class ErrorCode : int {
|
||||
Success = 0,
|
||||
PipeClosed = 1,
|
||||
ReadCorrupt = 2,
|
||||
};
|
||||
|
||||
enum class Opcode : uint32_t {
|
||||
Handshake = 0,
|
||||
Frame = 1,
|
||||
Close = 2,
|
||||
Ping = 3,
|
||||
Pong = 4,
|
||||
};
|
||||
|
||||
struct MessageFrameHeader {
|
||||
Opcode opcode;
|
||||
uint32_t length;
|
||||
};
|
||||
|
||||
struct MessageFrame : public MessageFrameHeader {
|
||||
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
|
||||
};
|
||||
|
||||
enum class State : uint32_t {
|
||||
Disconnected,
|
||||
SentHandshake,
|
||||
AwaitingResponse,
|
||||
Connected,
|
||||
};
|
||||
|
||||
BaseConnection *connection { nullptr };
|
||||
State state { State::Disconnected };
|
||||
void (*onConnect)(JsonDocument &message) { nullptr };
|
||||
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
|
||||
char appId[64] {};
|
||||
int lastErrorCode { 0 };
|
||||
char lastErrorMessage[256] {};
|
||||
RpcConnection::MessageFrame sendFrame;
|
||||
|
||||
static RpcConnection *Create(const char *applicationId);
|
||||
static void Destroy(RpcConnection *&);
|
||||
|
||||
inline bool IsOpen() const { return state == State::Connected; }
|
||||
|
||||
void Open();
|
||||
void Close();
|
||||
bool Write(const void *data, size_t length);
|
||||
bool Read(JsonDocument &message);
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
@@ -259,7 +259,16 @@ if(APPLE)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
find_package(getopt-win REQUIRED)
|
||||
find_package(getopt NAMES getopt getopt-win unofficial-getopt-win32 REQUIRED)
|
||||
if(TARGET getopt::getopt)
|
||||
set(GETOPT_LIBRARIES getopt::getopt)
|
||||
elseif(TARGET getopt-win::getopt)
|
||||
set(GETOPT_LIBRARIES getopt-win::getopt)
|
||||
elseif(TARGET getopt::getopt_shared)
|
||||
set(GETOPT_LIBRARIES getopt::getopt_shared)
|
||||
else()
|
||||
message(FATAL_ERROR "Missing getopt")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(APPLE OR WIN32)
|
||||
@@ -783,9 +792,7 @@ set(SOURCES
|
||||
src/scrobbler/scrobblercache.cpp
|
||||
src/scrobbler/scrobblercacheitem.cpp
|
||||
src/scrobbler/scrobblemetadata.cpp
|
||||
src/scrobbler/scrobblingapi20.cpp
|
||||
src/scrobbler/lastfmscrobbler.cpp
|
||||
src/scrobbler/librefmscrobbler.cpp
|
||||
src/scrobbler/listenbrainzscrobbler.cpp
|
||||
src/scrobbler/lastfmimport.cpp
|
||||
|
||||
@@ -1076,9 +1083,7 @@ set(HEADERS
|
||||
src/scrobbler/scrobblersettingsservice.h
|
||||
src/scrobbler/scrobblerservice.h
|
||||
src/scrobbler/scrobblercache.h
|
||||
src/scrobbler/scrobblingapi20.h
|
||||
src/scrobbler/lastfmscrobbler.h
|
||||
src/scrobbler/librefmscrobbler.h
|
||||
src/scrobbler/listenbrainzscrobbler.h
|
||||
src/scrobbler/lastfmimport.h
|
||||
|
||||
@@ -1494,7 +1499,7 @@ endif()
|
||||
|
||||
if(HAVE_DISCORD_RPC)
|
||||
add_subdirectory(3rdparty/discord-rpc)
|
||||
target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc/include)
|
||||
target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc)
|
||||
endif()
|
||||
|
||||
if(HAVE_TRANSLATIONS)
|
||||
@@ -1554,9 +1559,10 @@ target_link_libraries(strawberry_lib PUBLIC
|
||||
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
|
||||
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
|
||||
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
|
||||
$<$<BOOL:${WIN32}>:dsound dwmapi getopt-win::getopt>
|
||||
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
|
||||
$<$<BOOL:${MSVC}>:WindowsApp>
|
||||
KDAB::kdsingleapplication
|
||||
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
@@ -1575,10 +1581,6 @@ if(APPLE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(HAVE_DISCORD_RPC)
|
||||
target_link_libraries(strawberry_lib PRIVATE discord-rpc)
|
||||
endif()
|
||||
|
||||
target_link_libraries(strawberry PUBLIC strawberry_lib)
|
||||
|
||||
if(NOT APPLE)
|
||||
|
||||
91
Changelog
91
Changelog
@@ -2,6 +2,97 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
Version 1.2.14 (2025.10.25):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed showing error dialog minimized when main window is not current active window (#1739)
|
||||
* Fixed Discord timestamp update when seeking (#1813)
|
||||
* Fixed CD metadata lookup to respect MusicBrainz rate limiting
|
||||
* Fixed Tidal Open API cover provider
|
||||
* (Windows) Fixed device selection with WASAPI2
|
||||
|
||||
Enhancements/Other:
|
||||
* Removed libre.fm support
|
||||
* Rewrote MusicBrainzClient to use Json instead of XML
|
||||
* Subsonic will now use cover art from album when available
|
||||
* Added option to remove "Remastered", etc from song titles for Tidal, Qobuz and Spotify
|
||||
* Added webm to supported file extensions
|
||||
* (Windows|MinGW) Added WASAPI2 support
|
||||
* (Windows) Added experimental exclusive mode for WASAPI2
|
||||
|
||||
Version 1.2.13 (2025.08.31):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed playlist alternating row colors no longer working with some styles (#1806)
|
||||
* Fixed "Open Audio CD" no longer working (#1803)
|
||||
* Fixed systemtray icon playback status not working with scaling (#1782)
|
||||
* Fixed build without MusicBrainz (#1799)
|
||||
* Fixed build without MTP (#1804)
|
||||
|
||||
Enhancements:
|
||||
* Added Discord status text option (#1796)
|
||||
* Read Vorbis/FLAC "Other" embedded covers if front cover is not available (#1793)
|
||||
|
||||
Version 1.2.12 (2025.08.12):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed scrobbling for radio streams.
|
||||
* Fixed CDDA memory leaks.
|
||||
* Fixed device view CDDA loading (#1676).
|
||||
* Fixed collection directory editing (#1767).
|
||||
* Fixed devices sometimes being duplicated in the database.
|
||||
* Fixed alternating playlist row colors with Windows 11 style.
|
||||
* Fixed broken file filter for GME formats.
|
||||
* Fixed collection scanning for GME formats.
|
||||
* Fixed Chartlyrics.
|
||||
* Fixed network cache file descriptor leak on lyrics search with workaround for QTBUG-135641.
|
||||
* Fixed parsing Tidal urls with certain stream URL replies.
|
||||
* Fixed pixelated window icon on Wayland (#1753).
|
||||
* Fixed saving collection grouping with special characters in the name (#1758).
|
||||
* Fixed Spotify token not automatically updated on renewal when playing (#1769).
|
||||
* (macOS/Windows) Fixed network cache file descriptor leak with patch for QTBUG-135641.
|
||||
* (Windows|MSVC) Fixed installer to not restart the computer after installing Visual C++ Redistributable.
|
||||
|
||||
Enhancements:
|
||||
* Implemented edit tag dialog reset for year, track, disc and rating.
|
||||
* Added ALAC to supported filetypes for iPods.
|
||||
* Added CD-TEXT support.
|
||||
* Added back Genius lyrics.
|
||||
* Added support for reporting more info to ListenBrainz.
|
||||
* Added support for BPM, mood and initial key tags.
|
||||
* Added support for sort tags to collection, playlists and smart playlists.
|
||||
|
||||
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:
|
||||
|
||||
169
README.md
169
README.md
@@ -1,118 +1,135 @@
|
||||
:strawberry: Strawberry Music Player [](https://github.com/strawberrymusicplayer/strawberry/actions)
|
||||
=======================
|
||||
# :strawberry: Strawberry Music Player [](https://github.com/strawberrymusicplayer/strawberry/actions)
|
||||
[](https://github.com/sponsors/jonaski)
|
||||
[](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 framework.
|
||||
Strawberry is a **music player and music collection organizer**, originally forked from *Clementine* in 2018.
|
||||
It’s written in **C++ using the Qt framework**, designed for **audiophiles and music collectors**.
|
||||
|
||||

|
||||

|
||||
|
||||
Resources:
|
||||
---
|
||||
|
||||
* Website: https://www.strawberrymusicplayer.org/
|
||||
* Wiki: https://wiki.strawberrymusicplayer.org/
|
||||
* Forum: https://forum.strawberrymusicplayer.org/
|
||||
* Github: https://github.com/strawberrymusicplayer/strawberry
|
||||
* Latest builds: https://builds.strawberrymusicplayer.org/
|
||||
* openSUSE buildservice: https://build.opensuse.org/package/show/home:jonaski:audio/strawberry
|
||||
* Ubuntu PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||
* Ubuntu Unstable PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
|
||||
* Translations: https://crowdin.com/project/strawberrymusicplayer/
|
||||
## :globe_with_meridians: Resources
|
||||
|
||||
### :bangbang: Opening an issue
|
||||
- **Website:** https://www.strawberrymusicplayer.org
|
||||
- **Wiki:** https://wiki.strawberrymusicplayer.org
|
||||
- **Forum:** https://forum.strawberrymusicplayer.org
|
||||
- **GitHub:** https://github.com/strawberrymusicplayer/strawberry
|
||||
- **Latest builds:** https://builds.strawberrymusicplayer.org
|
||||
- **openSUSE Build Service:** https://build.opensuse.org/package/show/home:jonaski:audio/strawberry
|
||||
- **Ubuntu PPAs:**
|
||||
- Stable: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||
- Unstable: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
|
||||
- **Translations:** https://crowdin.com/project/strawberrymusicplayer
|
||||
|
||||
* Read the FAQ: https://wiki.strawberrymusicplayer.org/wiki/FAQ
|
||||
* Search for the issue to see if it is already solved, or if there is an open issue for it already. If there is an open issue already, you can comment on it if you have additional information that could be useful to us.
|
||||
* For technical problems, discussion, questions and feature suggestions use the forum (https://forum.strawberrymusicplayer.org/) instead. The forum is better suited for discussion.
|
||||
* We do not take feature requests from users on GitHub. Any issues related to feature requests will be closed. This does not necessarily mean that we won't add new features, but we don't have time to take feature requests or answer questions about new features from users. It is still possible to suggest or discuss new features on the forum (https://forum.strawberrymusicplayer.org/).
|
||||
* We do not maintain the Flatpak package. Do not report issues related to Flatpak unless the issue can be reproduced with a native package, use Flatpak support instead https://flatpak.org/about/
|
||||
---
|
||||
|
||||
### :moneybag: Sponsoring
|
||||
## :warning: Opening an Issue
|
||||
|
||||
The program is free software, released under GPL. If you like this program and can make use of it, consider sponsoring or donating to help fund the project.
|
||||
There are currently 4 options for sponsoring:
|
||||
Before creating a new GitHub issue:
|
||||
|
||||
1. **Read the [FAQ](https://wiki.strawberrymusicplayer.org/wiki/FAQ)**.
|
||||
2. **Search existing issues** to avoid duplicates. If one already exists, comment there with any additional information.
|
||||
3. **Use the [forum](https://forum.strawberrymusicplayer.org/)** for technical problems, discussions or feature suggestions — it’s better suited for back-and-forth conversation.
|
||||
4. **Feature requests are not accepted on GitHub.** Issues created for feature requests will be closed. You can still discuss ideas on the forum.
|
||||
5. **Flatpak users:** We do **not** maintain the Flatpak package. Report Flatpak-specific issues via [Flatpak support](https://flatpak.org/about/).
|
||||
|
||||
---
|
||||
|
||||
## :moneybag: Sponsoring
|
||||
|
||||
Strawberry is **free software released under the GPL**.
|
||||
If you enjoy using it, please consider **supporting development** through sponsorship or donation.
|
||||
|
||||
**Sponsorship options:**
|
||||
1. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||
2. [GitHub](https://github.com/sponsors/jonaski)
|
||||
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
||||
4. [PayPal](https://paypal.me/jonaskvinge)
|
||||
|
||||
Funding developers is a way to contribute to open source projects you appreciate, it helps developers get the resources they need, and recognize contributors working behind the scenes to make open source better for everyone.
|
||||
Supporting open-source developers helps ensure continued maintenance and improvements.
|
||||
|
||||
### :heavy_check_mark: Features
|
||||
---
|
||||
|
||||
* Play and organize music
|
||||
* Supports WAV, FLAC, WavPack, Ogg FLAC, Ogg Vorbis, Ogg Opus, Ogg Speex, MPC, TrueAudio, AIFF, MP4, MP3, ASF and Monkey's Audio.
|
||||
* Audio CD playback
|
||||
* Native desktop notifications
|
||||
* Playlist management
|
||||
* Smart and dynamic playlists
|
||||
* Advanced audio output and device configuration for bit-perfect playback on Linux
|
||||
* In-player song loudness analysis and song playback loudness normalization, as per EBU R 128
|
||||
* 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/)
|
||||
* 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/)
|
||||
* Streaming from Subsonic compatible servers
|
||||
* Unofficial Tidal, Spotify and Qobuz integration
|
||||
* Discord rich presence
|
||||
## :white_check_mark: Features
|
||||
|
||||
- Play and organize your music collection
|
||||
- Supports formats: WAV, FLAC, WavPack, Ogg Vorbis, Opus, MPC, TrueAudio, AIFF, MP4, MP3, ASF, and Monkey’s Audio
|
||||
- Audio CD playback
|
||||
- Bit-perfect playback on Linux
|
||||
- Native desktop notifications
|
||||
- Advanced playlist management
|
||||
- Smart and dynamic playlists
|
||||
- Loudness analysis and EBU R128 normalization
|
||||
- Editing tags and fetching missing tags via [MusicBrainz](https://musicbrainz.org/)
|
||||
- Album 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/), [Spotify](https://www.spotify.com/)
|
||||
- Lyrics from: [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics](https://www.lololyrics.com/), [songlyrics](https://www.songlyrics.com/), [azlyrics](https://www.azlyrics.com/), [elyrics](https://www.elyrics.net/), [letras](https://www.letras.mus.br), [LyricFind](https://lyrics.lyricfind.com)
|
||||
- Audio analyzer and equalizer
|
||||
- Transfer music to USB, MTP and iPod devices
|
||||
- Scrobbling to [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
||||
- Streaming from Subsonic-compatible servers
|
||||
- Unofficial integrations: Tidal, Spotify, and Qobuz
|
||||
- Discord Rich Presence
|
||||
|
||||
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
|
||||
---
|
||||
|
||||
**Access to macOS and Windows releases are currently restricted to sponsors, a 5 USD monthly sponsorship is required. You can sponsor strawberry through <a href="https://www.patreon.com/jonaskvinge">Patreon</a> for direct access to new releases. If you are sponsoring through GitHub, Ko-fi or PayPal, please e-mail support AT strawberrymusicplayer.org for access to downloads.**
|
||||
:white_check_mark: Tested on **Linux**, **OpenBSD**, **FreeBSD**, **macOS**, and **Windows**.
|
||||
|
||||
### :heavy_exclamation_mark: Requirements
|
||||
> **Note:** macOS and Windows releases are currently **available to sponsors only**.
|
||||
> A monthly sponsorship via [Patreon](https://www.patreon.com/jonaskvinge) grants direct access to new releases.
|
||||
|
||||
To build Strawberry from source you need the following installed on your system with the additional development packages/headers:
|
||||
---
|
||||
|
||||
* [CMake 3.13 or higher](https://cmake.org/)
|
||||
* 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/)
|
||||
* [GLib](https://developer.gnome.org/glib/)
|
||||
* [Qt 6.4.0 or higher with components Core, Concurrent, Gui, Widgets, Network, Sql and D-Bus](https://www.qt.io/)
|
||||
* [SQLite 3.9 or newer](https://www.sqlite.org)
|
||||
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
|
||||
* [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
* [TagLib 1.12 or higher](https://www.taglib.org/)
|
||||
* [ICU](https://unicode-org.github.io/icu/)
|
||||
* [KDSingleApplication 1.1.0 or higher](https://github.com/KDAB/KDSingleApplication)
|
||||
## :gear: Requirements
|
||||
|
||||
Optional dependencies:
|
||||
To build Strawberry from source, you’ll need:
|
||||
|
||||
* Song fingerprinting and MusicBrainz tagging: [Chromaprint](https://acoustid.org/chromaprint)
|
||||
* Moodbar: [fftw3](http://www.fftw.org/)
|
||||
* PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/?)
|
||||
* Audio CD: [libcdio](https://www.gnu.org/software/libcdio/)
|
||||
* 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/)
|
||||
**Dependencies:**
|
||||
- [CMake ≥= 3.13](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/)
|
||||
- [GLib](https://developer.gnome.org/glib/)
|
||||
- [Qt ≥= 6.4](https://www.qt.io/) (Core, Concurrent, Gui, Widgets, Network, SQL, D-Bus)
|
||||
- [SQLite ≥= 3.9](https://www.sqlite.org)
|
||||
- [ALSA (Linux only)](https://www.alsa-project.org/)
|
||||
- [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
- [TagLib ≥= 1.12](https://www.taglib.org/)
|
||||
- [ICU](https://unicode-org.github.io/icu/)
|
||||
- [KDSingleApplication ≥= 1.1.0](https://github.com/KDAB/KDSingleApplication)
|
||||
|
||||
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.
|
||||
**Dependencies for optional features:**
|
||||
- Fingerprinting & tagging: [Chromaprint](https://acoustid.org/chromaprint)
|
||||
- Moodbar: [FFTW3](http://www.fftw.org/)
|
||||
- PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/)
|
||||
- Audio CD support: [libcdio](https://www.gnu.org/software/libcdio/)
|
||||
- MTP devices: [libmtp](http://libmtp.sourceforge.net/)
|
||||
- iPod Classic: [libgpod](http://www.gtkpod.org/libgpod/)
|
||||
- EBU R128 normalization: [libebur128](https://github.com/jiixyj/libebur128)
|
||||
- Discord presence: [RapidJSON](https://rapidjson.org/)
|
||||
|
||||
### :wrench: Build from source
|
||||
Also install GStreamer plugins **base**, **good**, and optionally **bad**, **ugly** and **libav** for full codec support.
|
||||
|
||||
### Get the code:
|
||||
---
|
||||
|
||||
## :wrench: Build from Source
|
||||
|
||||
**Get the code:**
|
||||
|
||||
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
|
||||
|
||||
### Build and install:
|
||||
**Build and install:**
|
||||
|
||||
cd strawberry
|
||||
cmake -S . -B build
|
||||
cmake --build build --parallel $(nproc)
|
||||
sudo cmake --install build
|
||||
|
||||
To build on Windows with Visual Studio 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
For building on Windows with Visual Studio 2022, see: :point_right: https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
|
||||
### :penguin: Packaging status
|
||||
---
|
||||
|
||||
## :package: Packaging status
|
||||
|
||||
[](https://repology.org/metapackage/strawberry/versions)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 2)
|
||||
set(STRAWBERRY_VERSION_PATCH 8)
|
||||
set(STRAWBERRY_VERSION_PATCH 14)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<file>schema/schema-18.sql</file>
|
||||
<file>schema/schema-19.sql</file>
|
||||
<file>schema/schema-20.sql</file>
|
||||
<file>schema/schema-21.sql</file>
|
||||
<file>schema/device-schema.sql</file>
|
||||
<file>style/strawberry.css</file>
|
||||
<file>style/smartplaylistsearchterm.css</file>
|
||||
|
||||
@@ -12,9 +12,13 @@ CREATE TABLE device_%deviceid_subdirectories (
|
||||
CREATE TABLE device_%deviceid_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -22,7 +26,9 @@ CREATE TABLE device_%deviceid_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -86,7 +92,11 @@ CREATE TABLE device_%deviceid_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -94,4 +104,4 @@ CREATE INDEX idx_device_%deviceid_songs_album ON device_%deviceid_songs (album);
|
||||
|
||||
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
|
||||
|
||||
UPDATE devices SET schema_version=5 WHERE ROWID=%deviceid;
|
||||
UPDATE devices SET schema_version=6 WHERE ROWID=%deviceid;
|
||||
|
||||
43
data/schema/schema-21.sql
Normal file
43
data/schema/schema-21.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
DROP INDEX IF EXISTS idx_albumartistsort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_albumsort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_artistsort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_composersort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_performersort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_titlesort;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN albumartistsort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN albumsort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN artistsort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN composersort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN performersort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN titlesort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN bpm REAL;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN mood TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN initial_key TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
|
||||
|
||||
UPDATE schema_version SET version=21;
|
||||
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
||||
|
||||
DELETE FROM schema_version;
|
||||
|
||||
INSERT INTO schema_version (version) VALUES (20);
|
||||
INSERT INTO schema_version (version) VALUES (21);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS directories (
|
||||
path TEXT NOT NULL,
|
||||
@@ -20,9 +20,13 @@ CREATE TABLE IF NOT EXISTS subdirectories (
|
||||
CREATE TABLE IF NOT EXISTS songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -30,7 +34,9 @@ CREATE TABLE IF NOT EXISTS songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -94,16 +100,24 @@ CREATE TABLE IF NOT EXISTS songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -111,7 +125,9 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -175,16 +191,24 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -192,7 +216,9 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -256,16 +282,24 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -273,7 +307,9 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -337,16 +373,24 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -354,7 +398,9 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -418,16 +464,24 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -435,7 +489,9 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -499,16 +555,24 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -516,7 +580,9 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -580,16 +646,24 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -597,7 +671,9 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -661,16 +737,24 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -678,7 +762,9 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -742,16 +828,24 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -759,7 +853,9 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -823,16 +919,24 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -840,7 +944,9 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -904,7 +1010,11 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -931,9 +1041,13 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
playlist_url TEXT,
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER,
|
||||
disc INTEGER,
|
||||
year INTEGER,
|
||||
@@ -941,7 +1055,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
genre TEXT,
|
||||
compilation INTEGER DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -1005,7 +1121,11 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -1032,10 +1152,22 @@ CREATE INDEX IF NOT EXISTS idx_comp_artist ON songs (compilation_effective, arti
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumartist ON songs (albumartist);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artist ON songs (artist);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
|
||||
|
||||
CREATE VIEW IF NOT EXISTS duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;
|
||||
|
||||
4
debian/control
vendored
4
debian/control
vendored
@@ -60,11 +60,11 @@ 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 Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
- Unofficial streaming support for Tidal and Qobuz
|
||||
.
|
||||
|
||||
22
dist/macos/macversion.sh
vendored
22
dist/macos/macversion.sh
vendored
@@ -1,22 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
macos_version=$(sw_vers -productVersion)
|
||||
macos_version_major=$(echo $macos_version | awk -F '[.]' '{print $1}')
|
||||
macos_version_minor=$(echo $macos_version | awk -F '[.]' '{print $2}')
|
||||
|
||||
if [ "${macos_version_major}" = "10" ]; then
|
||||
macos_codenames=(
|
||||
["13"]="highsierra"
|
||||
["14"]="mojave"
|
||||
["15"]="catalina"
|
||||
)
|
||||
if [[ -n "${macos_codenames[$macos_version_minor]}" ]]; then
|
||||
echo "${macos_codenames[$macos_version_minor]}"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
elif [ "${macos_version_major}" = "11" ]; then
|
||||
echo "bigsur"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
@@ -31,10 +31,10 @@
|
||||
<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 Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind</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>
|
||||
<li>Scrobbler with support for Last.fm and ListenBrainz</li>
|
||||
<li>Streaming support for Subsonic-compatible servers</li>
|
||||
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
||||
</ul>
|
||||
@@ -51,6 +51,12 @@
|
||||
</screenshots>
|
||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||
<releases>
|
||||
<release version="1.2.14" date="2025-10-25"/>
|
||||
<release version="1.2.13" date="2025-08-31"/>
|
||||
<release version="1.2.12" date="2025-08-12"/>
|
||||
<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"/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
4
dist/unix/strawberry.1
vendored
4
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 Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
||||
.br
|
||||
- Support for multiple backends
|
||||
.br
|
||||
@@ -39,7 +39,7 @@ Features:
|
||||
.br
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
.br
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
.br
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
.br
|
||||
|
||||
6
dist/unix/strawberry.spec.in
vendored
6
dist/unix/strawberry.spec.in
vendored
@@ -93,16 +93,16 @@ 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 Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
||||
- Support for multiple backends
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
- Unofficial streaming support for Tidal and Qobuz
|
||||
|
||||
%if 0%{?suse_version} && 0%{?suse_version} <= 1600
|
||||
%if 0%{?suse_version} && 0%{?suse_version} < 1600
|
||||
%debug_package
|
||||
%endif
|
||||
|
||||
|
||||
103
dist/windows/strawberry.nsi.in
vendored
103
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
|
||||
@@ -214,7 +224,7 @@ Function InstallMSVCRuntime
|
||||
; ${If} $R0 == ""
|
||||
SetDetailsView hide
|
||||
; inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
|
||||
ExecWait '"$TEMP\${vc_redist_file}" /install /passive'
|
||||
ExecWait '"$TEMP\${vc_redist_file}" /install /passive /norestart'
|
||||
Delete "$TEMP\${vc_redist_file}"
|
||||
SetDetailsView show
|
||||
; ${EndIf}
|
||||
@@ -324,7 +334,7 @@ Section "Strawberry" Strawberry
|
||||
File "libqtsparkle-qt6.dll"
|
||||
File "libsoup-3.0-0.dll"
|
||||
File "libspeex-1.dll"
|
||||
File "libsqlite3.dll"
|
||||
File "libsqlite3-0.dll"
|
||||
File "libssp-0.dll"
|
||||
File "libstdc++-6.dll"
|
||||
File "libtag.dll"
|
||||
@@ -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,6 @@ Section "Strawberry" Strawberry
|
||||
File "glib-2.0-0.dll"
|
||||
File "gme.dll"
|
||||
File "gmodule-2.0-0.dll"
|
||||
File "gnutls.dll"
|
||||
File "gobject-2.0-0.dll"
|
||||
File "gstadaptivedemux-1.0-0.dll"
|
||||
File "gstapp-1.0-0.dll"
|
||||
@@ -402,14 +415,11 @@ Section "Strawberry" Strawberry
|
||||
File "gsttag-1.0-0.dll"
|
||||
File "gsturidownloader-1.0-0.dll"
|
||||
File "gstvideo-1.0-0.dll"
|
||||
File "gstwinrt-1.0-0.dll"
|
||||
File "harfbuzz.dll"
|
||||
File "intl-8.dll"
|
||||
File "jpeg62.dll"
|
||||
File "kdsingleapplication-qt6.dll"
|
||||
File "libbs2b.dll"
|
||||
File "libfaac_dll.dll"
|
||||
File "liblzma.dll"
|
||||
File "libmp3lame.dll"
|
||||
File "libopenmpt.dll"
|
||||
File "mpcdec.dll"
|
||||
@@ -427,6 +437,11 @@ Section "Strawberry" Strawberry
|
||||
File "vorbisfile.dll"
|
||||
File "wavpackdll.dll"
|
||||
|
||||
!ifndef arch_arm64
|
||||
File "gnutls.dll"
|
||||
File "libfaac_dll.dll"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
File "freetype.dll"
|
||||
File "libiconv.dll"
|
||||
@@ -434,8 +449,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeex.dll"
|
||||
File "pcre2-8.dll"
|
||||
File "pcre2-16.dll"
|
||||
File "zlib1.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolame.dll"
|
||||
File "zlib.dll"
|
||||
!endif
|
||||
!endif
|
||||
!ifdef debug
|
||||
File "freetyped.dll"
|
||||
@@ -444,8 +461,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeexd.dll"
|
||||
File "pcre2-8d.dll"
|
||||
File "pcre2-16d.dll"
|
||||
File "zlibd1.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolamed.dll"
|
||||
File "zlibd.dll"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
|
||||
@@ -459,7 +478,11 @@ Section "Strawberry" Strawberry
|
||||
; Common files
|
||||
|
||||
File "icudt77.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
File "fftw3.dll"
|
||||
!else
|
||||
File "libfftw3-3.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
File "icuin77d.dll"
|
||||
File "icuuc77d.dll"
|
||||
@@ -526,11 +549,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
|
||||
|
||||
@@ -647,6 +672,7 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=libgstvolume.dll" "gstreamer-plugins\libgstvolume.dll"
|
||||
File "/oname=libgstvorbis.dll" "gstreamer-plugins\libgstvorbis.dll"
|
||||
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
|
||||
File "/oname=libgstwasapi2.dll" "gstreamer-plugins\libgstwasapi2.dll"
|
||||
File "/oname=libgstwaveform.dll" "gstreamer-plugins\libgstwaveform.dll"
|
||||
File "/oname=libgstwavenc.dll" "gstreamer-plugins\libgstwavenc.dll"
|
||||
File "/oname=libgstwavpack.dll" "gstreamer-plugins\libgstwavpack.dll"
|
||||
@@ -674,7 +700,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"
|
||||
@@ -707,7 +732,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"
|
||||
@@ -719,8 +743,12 @@ 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"
|
||||
File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
|
||||
!endif
|
||||
!endif ; MSVC
|
||||
|
||||
@@ -849,7 +877,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libqtsparkle-qt6.dll"
|
||||
Delete "$INSTDIR\libsoup-3.0-0.dll"
|
||||
Delete "$INSTDIR\libspeex-1.dll"
|
||||
Delete "$INSTDIR\libsqlite3.dll"
|
||||
Delete "$INSTDIR\libsqlite3-0.dll"
|
||||
Delete "$INSTDIR\libssp-0.dll"
|
||||
Delete "$INSTDIR\libstdc++-6.dll"
|
||||
Delete "$INSTDIR\libtag.dll"
|
||||
@@ -892,6 +920,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"
|
||||
@@ -906,7 +938,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\glib-2.0-0.dll"
|
||||
Delete "$INSTDIR\gme.dll"
|
||||
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
Delete "$INSTDIR\gobject-2.0-0.dll"
|
||||
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstapp-1.0-0.dll"
|
||||
@@ -927,14 +958,11 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gsttag-1.0-0.dll"
|
||||
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstvideo-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstwinrt-1.0-0.dll"
|
||||
Delete "$INSTDIR\harfbuzz.dll"
|
||||
Delete "$INSTDIR\intl-8.dll"
|
||||
Delete "$INSTDIR\jpeg62.dll"
|
||||
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
|
||||
Delete "$INSTDIR\libbs2b.dll"
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
Delete "$INSTDIR\liblzma.dll"
|
||||
Delete "$INSTDIR\libmp3lame.dll"
|
||||
Delete "$INSTDIR\libopenmpt.dll"
|
||||
Delete "$INSTDIR\mpcdec.dll"
|
||||
@@ -952,6 +980,11 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\vorbisfile.dll"
|
||||
Delete "$INSTDIR\wavpackdll.dll"
|
||||
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
Delete "$INSTDIR\freetype.dll"
|
||||
Delete "$INSTDIR\libiconv.dll"
|
||||
@@ -959,8 +992,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeex.dll"
|
||||
Delete "$INSTDIR\pcre2-8.dll"
|
||||
Delete "$INSTDIR\pcre2-16.dll"
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolame.dll"
|
||||
Delete "$INSTDIR\zlib.dll"
|
||||
!endif
|
||||
!endif
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\freetyped.dll"
|
||||
@@ -969,8 +1004,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeexd.dll"
|
||||
Delete "$INSTDIR\pcre2-8d.dll"
|
||||
Delete "$INSTDIR\pcre2-16d.dll"
|
||||
Delete "$INSTDIR\zlibd1.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolamed.dll"
|
||||
Delete "$INSTDIR\zlibd.dll"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!ifdef arch_x86
|
||||
@@ -983,7 +1020,11 @@ Section "Uninstall"
|
||||
; Common files
|
||||
|
||||
Delete "$INSTDIR\icudt77.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
Delete "$INSTDIR\fftw3.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libfftw3-3.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
Delete "$INSTDIR\icuin77d.dll"
|
||||
Delete "$INSTDIR\icuuc77d.dll"
|
||||
@@ -1016,11 +1057,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
|
||||
@@ -1104,6 +1147,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstvolume.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstvorbis.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi2.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwaveform.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavenc.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavpack.dll"
|
||||
@@ -1133,7 +1177,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"
|
||||
@@ -1166,7 +1209,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"
|
||||
@@ -1178,9 +1220,14 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
|
||||
!ifdef arch_x64
|
||||
;Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
|
||||
!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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QRegularExpression>
|
||||
#include <QInputDialog>
|
||||
#include <QList>
|
||||
@@ -295,19 +296,21 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
|
||||
if (version == 1) {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(name).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
ds >> g;
|
||||
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
|
||||
ret->addAction(CreateGroupByAction(QUrl::fromPercentEncoding(name.toUtf8()), parent, g));
|
||||
}
|
||||
}
|
||||
else {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
s.remove(saved.at(i));
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
s.remove(name);
|
||||
}
|
||||
}
|
||||
s.endGroup();
|
||||
@@ -339,7 +342,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
||||
|
||||
if (!model_) return;
|
||||
|
||||
QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
const QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
if (name.isEmpty()) return;
|
||||
|
||||
qLog(Debug) << "Saving current grouping to" << name;
|
||||
@@ -355,7 +358,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
||||
QDataStream datastream(&buffer, QIODevice::WriteOnly);
|
||||
datastream << model_->GetGroupBy();
|
||||
s.setValue("version", u"1"_s);
|
||||
s.setValue(name, buffer);
|
||||
s.setValue(QUrl::toPercentEncoding(name), buffer);
|
||||
s.endGroup();
|
||||
|
||||
UpdateGroupByActions();
|
||||
|
||||
@@ -78,6 +78,8 @@ CollectionLibrary::CollectionLibrary(const SharedPtr<Database> database,
|
||||
|
||||
model_ = new CollectionModel(backend_, albumcover_loader, this);
|
||||
|
||||
full_rescan_revisions_[21] = tr("Support for sort tags artist, album, album artist, title, composer, and performer");
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* 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
|
||||
@@ -54,6 +54,7 @@
|
||||
|
||||
#include "includes/scoped_ptr.h"
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "constants/collectionsettings.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/standardpaths.h"
|
||||
#include "core/database.h"
|
||||
@@ -71,12 +72,12 @@
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
#include "covermanager/albumcoverloader.h"
|
||||
#include "constants/collectionsettings.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
const int CollectionModel::kPrettyCoverSize = 32;
|
||||
|
||||
namespace {
|
||||
constexpr char kPixmapDiskCacheDir[] = "pixmapcache";
|
||||
constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists");
|
||||
@@ -88,7 +89,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
|
||||
albumcover_loader_(albumcover_loader),
|
||||
dir_model_(new CollectionDirectoryModel(backend, this)),
|
||||
filter_(new CollectionFilter(this)),
|
||||
timer_reload_(new QTimer(this)),
|
||||
timer_update_(new QTimer(this)),
|
||||
icon_artist_(IconLoader::Load(u"folder-sound"_s)),
|
||||
use_disk_cache_(false),
|
||||
@@ -130,10 +130,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
|
||||
backend_->UpdateTotalArtistCountAsync();
|
||||
backend_->UpdateTotalAlbumCountAsync();
|
||||
|
||||
timer_reload_->setSingleShot(true);
|
||||
timer_reload_->setInterval(300ms);
|
||||
QObject::connect(timer_reload_, &QTimer::timeout, this, &CollectionModel::Reload);
|
||||
|
||||
timer_update_->setSingleShot(false);
|
||||
timer_update_->setInterval(20ms);
|
||||
QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate);
|
||||
@@ -191,13 +187,9 @@ void CollectionModel::EndReset() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::Reload() {
|
||||
void CollectionModel::ResetInternal() {
|
||||
|
||||
loading_ = true;
|
||||
if (timer_reload_->isActive()) {
|
||||
timer_reload_->stop();
|
||||
}
|
||||
updates_.clear();
|
||||
|
||||
options_active_ = options_current_;
|
||||
|
||||
@@ -211,22 +203,15 @@ void CollectionModel::Reload() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ScheduleReset() {
|
||||
|
||||
if (!timer_reload_->isActive()) {
|
||||
timer_reload_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ReloadSettings() {
|
||||
|
||||
Settings settings;
|
||||
settings.beginGroup(CollectionSettings::kSettingsGroup);
|
||||
const bool show_pretty_covers = settings.value(CollectionSettings::kPrettyCovers, true).toBool();
|
||||
const bool show_dividers= settings.value(CollectionSettings::kShowDividers, true).toBool();
|
||||
const bool show_dividers = settings.value(CollectionSettings::kShowDividers, true).toBool();
|
||||
const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool();
|
||||
const bool sort_skips_articles = settings.value(CollectionSettings::kSortSkipsArticles, true).toBool();
|
||||
const bool sort_skip_articles_for_artists = settings.value(CollectionSettings::kSkipArticlesForArtists, true).toBool();
|
||||
const bool sort_skip_articles_for_albums = settings.value(CollectionSettings::kSkipArticlesForAlbums, false).toBool();
|
||||
|
||||
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
|
||||
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
|
||||
@@ -241,11 +226,13 @@ void CollectionModel::ReloadSettings() {
|
||||
if (show_pretty_covers != options_current_.show_pretty_covers ||
|
||||
show_dividers != options_current_.show_dividers ||
|
||||
show_various_artists != options_current_.show_various_artists ||
|
||||
sort_skips_articles != options_current_.sort_skips_articles) {
|
||||
sort_skip_articles_for_artists != options_current_.sort_skip_articles_for_artists ||
|
||||
sort_skip_articles_for_albums != options_current_.sort_skip_articles_for_albums) {
|
||||
options_current_.show_pretty_covers = show_pretty_covers;
|
||||
options_current_.show_dividers = show_dividers;
|
||||
options_current_.show_various_artists = show_various_artists;
|
||||
options_current_.sort_skips_articles = sort_skips_articles;
|
||||
options_current_.sort_skip_articles_for_artists = sort_skip_articles_for_artists;
|
||||
options_current_.sort_skip_articles_for_albums = sort_skip_articles_for_albums;
|
||||
ScheduleReset();
|
||||
}
|
||||
|
||||
@@ -421,10 +408,15 @@ void CollectionModel::RemoveSongs(const SongList &songs) {
|
||||
|
||||
void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) {
|
||||
|
||||
for (qint64 i = 0; i < songs.count(); i += 400LL) {
|
||||
const qint64 number = std::min(songs.count() - i, 400LL);
|
||||
const SongList songs_to_queue = songs.mid(i, number);
|
||||
updates_.enqueue(CollectionModelUpdate(type, songs_to_queue));
|
||||
if (type == CollectionModelUpdate::Type::Reset) {
|
||||
updates_.enqueue(CollectionModelUpdate(type));
|
||||
}
|
||||
else {
|
||||
for (qint64 i = 0; i < songs.count(); i += 400LL) {
|
||||
const qint64 number = std::min(songs.count() - i, 400LL);
|
||||
const SongList songs_to_queue = songs.mid(i, number);
|
||||
updates_.enqueue(CollectionModelUpdate(type, songs_to_queue));
|
||||
}
|
||||
}
|
||||
|
||||
if (!timer_update_->isActive()) {
|
||||
@@ -433,6 +425,14 @@ void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, con
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ScheduleReset() {
|
||||
|
||||
if (!updates_.isEmpty() && updates_.first().type == CollectionModelUpdate::Type::Reset) return;
|
||||
|
||||
ScheduleUpdate(CollectionModelUpdate::Type::Reset);
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ScheduleAddSongs(const SongList &songs) {
|
||||
|
||||
ScheduleUpdate(CollectionModelUpdate::Type::Add, songs);
|
||||
@@ -465,6 +465,9 @@ void CollectionModel::ProcessUpdate() {
|
||||
}
|
||||
|
||||
switch (update.type) {
|
||||
case CollectionModelUpdate::Type::Reset:
|
||||
ResetInternal();
|
||||
break;
|
||||
case CollectionModelUpdate::Type::AddReAddOrUpdate:
|
||||
AddReAddOrUpdateSongsInternal(update.songs);
|
||||
break;
|
||||
@@ -539,7 +542,10 @@ void CollectionModel::AddSongsInternal(const SongList &songs) {
|
||||
// Sanity check to make sure we don't add songs that are outside the user's filter
|
||||
if (!options_active_.filter_options.Matches(song)) continue;
|
||||
|
||||
if (song_nodes_.contains(song.id())) continue;
|
||||
if (song_nodes_.contains(song.id())) {
|
||||
qLog(Debug) << song.id() << song.title() << "already exists, skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Before we can add each song we need to make sure the required container items already exist in the tree.
|
||||
// These depend on which "group by" settings the user has on the collection.
|
||||
@@ -699,7 +705,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
||||
|
||||
QString divider_key;
|
||||
if (options_active_.show_dividers && container_level == 0) {
|
||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skips_articles));
|
||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums));
|
||||
if (!divider_key.isEmpty()) {
|
||||
if (!divider_nodes_.contains(divider_key)) {
|
||||
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
|
||||
@@ -713,7 +719,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
||||
item->container_level = container_level;
|
||||
item->container_key = container_key;
|
||||
item->display_text = DisplayText(group_by, song);
|
||||
item->sort_text = SortText(group_by, song, options_active_.sort_skips_articles);
|
||||
item->sort_text = SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums);
|
||||
if (!divider_key.isEmpty()) {
|
||||
item->sort_text.prepend(divider_key + QLatin1Char(' '));
|
||||
}
|
||||
@@ -1068,25 +1074,25 @@ QString CollectionModel::PrettyFormat(const Song &song) {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles) {
|
||||
QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums) {
|
||||
|
||||
switch (group_by) {
|
||||
case GroupBy::AlbumArtist:
|
||||
return SortTextForArtist(song.effective_albumartist(), sort_skips_articles);
|
||||
return SortTextForName(song.effective_albumartistsort(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Artist:
|
||||
return SortTextForArtist(song.artist(), sort_skips_articles);
|
||||
return SortTextForName(song.effective_artistsort(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Album:
|
||||
return SortText(song.album());
|
||||
return SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
|
||||
case GroupBy::AlbumDisc:
|
||||
return song.album() + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::YearAlbum:
|
||||
return SortTextForNumber(std::max(0, song.year())) + song.grouping() + song.album();
|
||||
return SortTextForNumber(std::max(0, song.year())) + song.grouping() + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
|
||||
case GroupBy::YearAlbumDisc:
|
||||
return SortTextForNumber(std::max(0, song.year())) + song.album() + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForNumber(std::max(0, song.year())) + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::OriginalYearAlbum:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.grouping() + song.album();
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.grouping() + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
|
||||
case GroupBy::OriginalYearAlbumDisc:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.album() + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::Disc:
|
||||
return SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::Year:
|
||||
@@ -1094,13 +1100,13 @@ QString CollectionModel::SortText(const GroupBy group_by, const Song &song, cons
|
||||
case GroupBy::OriginalYear:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
|
||||
case GroupBy::Genre:
|
||||
return SortTextForArtist(song.genre(), sort_skips_articles);
|
||||
return SortText(song.genre());
|
||||
case GroupBy::Composer:
|
||||
return SortTextForArtist(song.composer(), sort_skips_articles);
|
||||
return SortTextForName(song.effective_composersort(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Performer:
|
||||
return SortTextForArtist(song.performer(), sort_skips_articles);
|
||||
return SortTextForName(song.effective_performersort(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Grouping:
|
||||
return SortTextForArtist(song.grouping(), sort_skips_articles);
|
||||
return SortText(song.grouping());
|
||||
case GroupBy::FileType:
|
||||
return song.TextForFiletype();
|
||||
case GroupBy::Format:
|
||||
@@ -1135,21 +1141,9 @@ QString CollectionModel::SortText(QString text) {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortTextForArtist(QString artist, const bool skip_articles) {
|
||||
QString CollectionModel::SortTextForName(const QString &name, const bool sort_skip_articles) {
|
||||
|
||||
artist = SortText(artist);
|
||||
|
||||
if (skip_articles) {
|
||||
for (const auto &i : Song::kArticles) {
|
||||
if (artist.startsWith(i)) {
|
||||
qint64 ilen = i.length();
|
||||
artist = artist.right(artist.length() - ilen) + ", "_L1 + i.left(ilen - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return artist;
|
||||
return sort_skip_articles ? SkipArticles(SortText(name)) : SortText(name);
|
||||
|
||||
}
|
||||
|
||||
@@ -1180,6 +1174,20 @@ QString CollectionModel::SortTextForBitrate(const int bitrate) {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SkipArticles(QString name) {
|
||||
|
||||
for (const auto &i : Song::kArticles) {
|
||||
if (name.startsWith(i)) {
|
||||
qint64 ilen = i.length();
|
||||
name = name.right(name.length() - ilen) + ", "_L1 + i.left(ilen - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
|
||||
}
|
||||
|
||||
bool CollectionModel::IsSongTitleDataChanged(const Song &song1, const Song &song2) {
|
||||
|
||||
return song1.url() != song2.url() ||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* 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
|
||||
@@ -129,14 +129,16 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
show_dividers(true),
|
||||
show_pretty_covers(true),
|
||||
show_various_artists(true),
|
||||
sort_skips_articles(true),
|
||||
sort_skip_articles_for_artists(false),
|
||||
sort_skip_articles_for_albums(false),
|
||||
separate_albums_by_grouping(false) {}
|
||||
|
||||
Grouping group_by;
|
||||
bool show_dividers;
|
||||
bool show_pretty_covers;
|
||||
bool show_various_artists;
|
||||
bool sort_skips_articles;
|
||||
bool sort_skip_articles_for_artists;
|
||||
bool sort_skip_articles_for_albums;
|
||||
bool separate_albums_by_grouping;
|
||||
CollectionFilterOptions filter_options;
|
||||
};
|
||||
@@ -176,20 +178,21 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
|
||||
// Utility functions for manipulating text
|
||||
static QString DisplayText(const GroupBy group_by, const Song &song);
|
||||
QString DisplayText(const GroupBy group_by, const Song &song);
|
||||
static QString TextOrUnknown(const QString &text);
|
||||
static QString PrettyYearAlbum(const int year, const QString &album);
|
||||
static QString PrettyAlbumDisc(const QString &album, const int disc);
|
||||
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
||||
static QString PrettyDisc(const int disc);
|
||||
static QString PrettyFormat(const Song &song);
|
||||
QString SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles);
|
||||
QString SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums);
|
||||
static QString SortText(QString text);
|
||||
static QString SortTextForName(const QString &name, const bool sort_skip_articles);
|
||||
static QString SortTextForNumber(const int number);
|
||||
static QString SortTextForArtist(QString artist, const bool skip_articles);
|
||||
static QString SortTextForSong(const Song &song);
|
||||
static QString SortTextForYear(const int year);
|
||||
static QString SortTextForBitrate(const int bitrate);
|
||||
static QString SkipArticles(QString name);
|
||||
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
|
||||
QString ContainerKey(const GroupBy group_by, const Song &song, bool &has_unique_album_identifier) const;
|
||||
|
||||
@@ -228,7 +231,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
|
||||
QVariant data(CollectionItem *item, const int role) const;
|
||||
|
||||
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
|
||||
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs = SongList());
|
||||
void ScheduleAddSongs(const SongList &songs);
|
||||
void ScheduleUpdateSongs(const SongList &songs);
|
||||
void ScheduleRemoveSongs(const SongList &songs);
|
||||
@@ -259,7 +262,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
|
||||
|
||||
private Q_SLOTS:
|
||||
void Reload();
|
||||
void ResetInternal();
|
||||
void ScheduleReset();
|
||||
void ProcessUpdate();
|
||||
void LoadSongsFromSqlAsyncFinished();
|
||||
@@ -278,7 +281,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
const SharedPtr<AlbumCoverLoader> albumcover_loader_;
|
||||
CollectionDirectoryModel *dir_model_;
|
||||
CollectionFilter *filter_;
|
||||
QTimer *timer_reload_;
|
||||
QTimer *timer_update_;
|
||||
|
||||
QPixmap pixmap_no_cover_;
|
||||
|
||||
@@ -25,12 +25,13 @@
|
||||
class CollectionModelUpdate {
|
||||
public:
|
||||
enum class Type {
|
||||
Reset,
|
||||
AddReAddOrUpdate,
|
||||
Add,
|
||||
Update,
|
||||
Remove,
|
||||
};
|
||||
explicit CollectionModelUpdate(const Type _type, const SongList &_songs);
|
||||
explicit CollectionModelUpdate(const Type _type, const SongList &_songs = SongList());
|
||||
Type type;
|
||||
SongList songs;
|
||||
};
|
||||
|
||||
@@ -34,8 +34,6 @@ CollectionPlaylistItem::CollectionPlaylistItem(const Song::Source source) : Play
|
||||
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
|
||||
|
||||
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
|
||||
|
||||
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
|
||||
|
||||
int col = 0;
|
||||
@@ -62,7 +60,7 @@ void CollectionPlaylistItem::Reload() {
|
||||
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
|
||||
return;
|
||||
}
|
||||
UpdateTemporaryMetadata(song_);
|
||||
UpdateStreamMetadata(song_);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -78,16 +76,9 @@ QVariant CollectionPlaylistItem::DatabaseValue(const DatabaseColumn database_col
|
||||
|
||||
}
|
||||
|
||||
Song CollectionPlaylistItem::Metadata() const {
|
||||
|
||||
if (HasTemporaryMetadata()) return temp_metadata_;
|
||||
return song_;
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
@@ -35,19 +35,17 @@ class CollectionPlaylistItem : public PlaylistItem {
|
||||
explicit CollectionPlaylistItem(const Song::Source source);
|
||||
explicit CollectionPlaylistItem(const Song &song);
|
||||
|
||||
QUrl Url() const override;
|
||||
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; }
|
||||
|
||||
void SetArtManual(const QUrl &cover_url) override;
|
||||
|
||||
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
|
||||
|
||||
protected:
|
||||
QVariant DatabaseValue(const DatabaseColumn database_column) const override;
|
||||
Song DatabaseSongMetadata() const override { return Song(source_); }
|
||||
|
||||
@@ -65,10 +65,8 @@
|
||||
#include "collectionitem.h"
|
||||
#include "collectionitemdelegate.h"
|
||||
#include "collectionview.h"
|
||||
#ifndef Q_OS_WIN32
|
||||
# include "device/devicemanager.h"
|
||||
# include "device/devicestatefiltermodel.h"
|
||||
#endif
|
||||
#include "device/devicemanager.h"
|
||||
#include "device/devicestatefiltermodel.h"
|
||||
#include "dialogs/edittagdialog.h"
|
||||
#include "dialogs/deleteconfirmationdialog.h"
|
||||
#include "organize/organizedialog.h"
|
||||
@@ -95,9 +93,7 @@ CollectionView::CollectionView(QWidget *parent)
|
||||
action_open_in_new_playlist_(nullptr),
|
||||
action_organize_(nullptr),
|
||||
action_search_for_this_(nullptr),
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_(nullptr),
|
||||
#endif
|
||||
action_edit_track_(nullptr),
|
||||
action_edit_tracks_(nullptr),
|
||||
action_rescan_songs_(nullptr),
|
||||
@@ -417,9 +413,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
context_menu_->addSeparator();
|
||||
action_organize_ = context_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &CollectionView::Organize);
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &CollectionView::CopyToDevice);
|
||||
#endif
|
||||
action_delete_files_ = context_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &CollectionView::Delete);
|
||||
|
||||
context_menu_->addSeparator();
|
||||
@@ -439,10 +433,8 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
context_menu_->addMenu(filter_widget_->menu());
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_->setDisabled(device_manager_->connected_devices_model()->rowCount() == 0);
|
||||
QObject::connect(device_manager_->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, action_copy_to_device_, &QAction::setDisabled);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@@ -481,9 +473,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
action_rescan_songs_->setEnabled(regular_editable > 0);
|
||||
|
||||
action_organize_->setVisible(regular_elements == regular_editable);
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_->setVisible(regular_elements == regular_editable);
|
||||
#endif
|
||||
|
||||
action_delete_files_->setVisible(delete_files_);
|
||||
|
||||
@@ -492,9 +482,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
// only when all selected items are editable
|
||||
action_organize_->setEnabled(regular_elements == regular_editable);
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
|
||||
#endif
|
||||
|
||||
action_delete_files_->setEnabled(delete_files_);
|
||||
|
||||
@@ -759,7 +747,6 @@ void CollectionView::RescanSongs() {
|
||||
|
||||
void CollectionView::CopyToDevice() {
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
if (!organize_dialog_) {
|
||||
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
|
||||
}
|
||||
@@ -768,7 +755,6 @@ void CollectionView::CopyToDevice() {
|
||||
organize_dialog_->SetCopy(true);
|
||||
organize_dialog_->SetSongs(GetSelectedSongs());
|
||||
organize_dialog_->show();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ class CollectionView : public AutoExpandingTreeView {
|
||||
void DeleteFilesFinished(const SongList &songs_with_errors);
|
||||
|
||||
private:
|
||||
void RecheckIsEmpty();
|
||||
void SetShowInVarious(const bool on);
|
||||
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
|
||||
void SaveContainerPath(const QModelIndex &child);
|
||||
@@ -176,9 +175,7 @@ class CollectionView : public AutoExpandingTreeView {
|
||||
QAction *action_organize_;
|
||||
QAction *action_search_for_this_;
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
QAction *action_copy_to_device_;
|
||||
#endif
|
||||
QAction *action_edit_track_;
|
||||
QAction *action_edit_tracks_;
|
||||
QAction *action_rescan_songs_;
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QIODevice>
|
||||
#include <QDataStream>
|
||||
#include <QKeySequence>
|
||||
@@ -167,14 +168,20 @@ void SavedGroupingManager::UpdateModel() {
|
||||
if (version == 1) {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(name).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
ds >> g;
|
||||
|
||||
QList<QStandardItem*> list;
|
||||
list << new QStandardItem(saved.at(i))
|
||||
|
||||
QStandardItem *item = new QStandardItem();
|
||||
item->setText(QUrl::fromPercentEncoding(name.toUtf8()));
|
||||
item->setData(name);
|
||||
|
||||
list << item
|
||||
<< new QStandardItem(GroupByToString(g.first))
|
||||
<< new QStandardItem(GroupByToString(g.second))
|
||||
<< new QStandardItem(GroupByToString(g.third));
|
||||
@@ -185,8 +192,9 @@ void SavedGroupingManager::UpdateModel() {
|
||||
else {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
s.remove(saved.at(i));
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
s.remove(name);
|
||||
}
|
||||
}
|
||||
s.endGroup();
|
||||
@@ -202,7 +210,7 @@ void SavedGroupingManager::Remove() {
|
||||
for (const QModelIndex &idx : indexes) {
|
||||
if (idx.isValid()) {
|
||||
qLog(Debug) << "Remove saved grouping: " << model_->item(idx.row(), 0)->text();
|
||||
s.remove(model_->item(idx.row(), 0)->text());
|
||||
s.remove(model_->item(idx.row(), 0)->data().toString());
|
||||
}
|
||||
}
|
||||
s.endGroup();
|
||||
|
||||
@@ -33,6 +33,7 @@ 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";
|
||||
|
||||
@@ -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
|
||||
@@ -24,18 +24,20 @@ namespace CollectionSettings {
|
||||
|
||||
constexpr char kSettingsGroup[] = "Collection";
|
||||
|
||||
constexpr char kStartupScan[] = "startup_scan";
|
||||
constexpr char kMonitor[] = "monitor";
|
||||
constexpr char kSongTracking[] = "song_tracking";
|
||||
constexpr char kMarkSongsUnavailable[] = "mark_songs_unavailable";
|
||||
constexpr char kSongENUR128LoudnessAnalysis[] = "song_ebur128_loudness_analysis";
|
||||
constexpr char kExpireUnavailableSongs[] = "expire_unavailable_songs";
|
||||
constexpr char kCoverArtPatterns[] = "cover_art_patterns";
|
||||
constexpr char kAutoOpen[] = "auto_open";
|
||||
constexpr char kShowDividers[] = "show_dividers";
|
||||
constexpr char kPrettyCovers[] = "pretty_covers";
|
||||
constexpr char kVariousArtists[] = "various_artists";
|
||||
constexpr char kSortSkipsArticles[] = "sort_skips_articles";
|
||||
constexpr char kStartupScan[] = "startup_scan";
|
||||
constexpr char kMonitor[] = "monitor";
|
||||
constexpr char kSongTracking[] = "song_tracking";
|
||||
constexpr char kSongENUR128LoudnessAnalysis[] = "song_ebur128_loudness_analysis";
|
||||
constexpr char kMarkSongsUnavailable[] = "mark_songs_unavailable";
|
||||
constexpr char kExpireUnavailableSongs[] = "expire_unavailable_songs";
|
||||
constexpr char kCoverArtPatterns[] = "cover_art_patterns";
|
||||
constexpr char kSkipArticlesForArtists[] = "skip_articles_for_artists";
|
||||
constexpr char kSkipArticlesForAlbums[] = "skip_articles_for_albums";
|
||||
constexpr char kShowSortText[] = "show_sort_text";
|
||||
constexpr char kSettingsCacheSize[] = "cache_size";
|
||||
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
|
||||
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";
|
||||
|
||||
@@ -27,10 +27,10 @@ constexpr char kAllFilesFilterSpec[] = QT_TRANSLATE_NOOP("FileFilter", "All File
|
||||
constexpr char kFileFilter[] =
|
||||
"*.wav *.flac *.wv *.ogg *.oga *.opus *.spx *.ape *.mpc "
|
||||
"*.mp2 *.mp3 *.m4a *.mp4 *.aac *.asf *.asx *.wma "
|
||||
"*.aif *.aiff *.mka *.tta *.dsf *.dsd "
|
||||
"*.aif *.aiff *.mka *.tta *.dsf *.dsd *.webm "
|
||||
"*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini "
|
||||
"*.ac3 *.dts "
|
||||
"*.mod *.s3m *.xm *.it"
|
||||
"*.mod *.s3m *.xm *.it "
|
||||
"*.spc *.vgm";
|
||||
|
||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
|
||||
|
||||
@@ -71,6 +71,14 @@ constexpr char kSettingsGroup[] = "DiscordRPC";
|
||||
|
||||
constexpr char kEnabled[] = "enabled";
|
||||
|
||||
constexpr char kStatusDisplayType[] = "StatusDisplayType";
|
||||
|
||||
enum class StatusDisplayType {
|
||||
App = 0,
|
||||
Artist,
|
||||
Song
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // NOTIFICATIONSSETTINGS_H
|
||||
|
||||
@@ -36,6 +36,7 @@ constexpr char kAlbumsSearchLimit[] = "albumssearchlimit";
|
||||
constexpr char kSongsSearchLimit[] = "songssearchlimit";
|
||||
constexpr char kBase64Secret[] = "base64secret";
|
||||
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
|
||||
constexpr char kRemoveRemastered[] = "remove_remastered";
|
||||
|
||||
constexpr char kUserId[] = "user_id";
|
||||
constexpr char kCredentialsId[] = "credentials_id";
|
||||
|
||||
@@ -31,6 +31,7 @@ constexpr char kAlbumsSearchLimit[] = "albumssearchlimit";
|
||||
constexpr char kSongsSearchLimit[] = "songssearchlimit";
|
||||
constexpr char kFetchAlbums[] = "fetchalbums";
|
||||
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
|
||||
constexpr char kRemoveRemastered[] = "remove_remastered";
|
||||
|
||||
constexpr char kAccessToken[] = "access_token";
|
||||
constexpr char kRefreshToken[] = "refresh_token";
|
||||
|
||||
@@ -40,6 +40,7 @@ constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
|
||||
constexpr char kCoverSize[] = "coversize";
|
||||
constexpr char kStreamUrl[] = "streamurl";
|
||||
constexpr char kAlbumExplicit[] = "album_explicit";
|
||||
constexpr char kRemoveRemastered[] = "remove_remastered";
|
||||
|
||||
enum class StreamUrlMethod {
|
||||
StreamUrl,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2013-2022, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2013-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
|
||||
@@ -100,15 +100,11 @@ ContextView::ContextView(QWidget *parent)
|
||||
label_samplerate_title_(new QLabel(this)),
|
||||
label_bitdepth_title_(new QLabel(this)),
|
||||
label_bitrate_title_(new QLabel(this)),
|
||||
label_ebur128_integrated_loudness_title_(new QLabel(this)),
|
||||
label_ebur128_loudness_range_title_(new QLabel(this)),
|
||||
label_filetype_(new QLabel(this)),
|
||||
label_length_(new QLabel(this)),
|
||||
label_samplerate_(new QLabel(this)),
|
||||
label_bitdepth_(new QLabel(this)),
|
||||
label_bitrate_(new QLabel(this)),
|
||||
label_ebur128_integrated_loudness_(new QLabel(this)),
|
||||
label_ebur128_loudness_range_(new QLabel(this)),
|
||||
lyrics_tried_(false),
|
||||
lyrics_id_(-1) {
|
||||
|
||||
@@ -166,24 +162,18 @@ ContextView::ContextView(QWidget *parent)
|
||||
label_samplerate_title_->setText(tr("Samplerate"));
|
||||
label_bitdepth_title_->setText(tr("Bit depth"));
|
||||
label_bitrate_title_->setText(tr("Bitrate"));
|
||||
label_ebur128_integrated_loudness_title_->setText(tr("EBU R 128 Integrated Loudness"));
|
||||
label_ebur128_loudness_range_title_->setText(tr("EBU R 128 Loudness Range"));
|
||||
|
||||
label_filetype_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
label_length_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
label_samplerate_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
label_bitdepth_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
label_bitrate_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
label_ebur128_integrated_loudness_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
label_ebur128_loudness_range_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
|
||||
label_filetype_->setWordWrap(true);
|
||||
label_length_->setWordWrap(true);
|
||||
label_samplerate_->setWordWrap(true);
|
||||
label_bitdepth_->setWordWrap(true);
|
||||
label_bitrate_->setWordWrap(true);
|
||||
label_ebur128_integrated_loudness_->setWordWrap(true);
|
||||
label_ebur128_loudness_range_->setWordWrap(true);
|
||||
|
||||
layout_play_data_->setContentsMargins(0, 0, 0, 0);
|
||||
layout_play_data_->addWidget(label_filetype_title_, 0, 0);
|
||||
@@ -197,11 +187,6 @@ ContextView::ContextView(QWidget *parent)
|
||||
layout_play_data_->addWidget(label_bitrate_title_, 4, 0);
|
||||
layout_play_data_->addWidget(label_bitrate_, 4, 1);
|
||||
|
||||
layout_play_data_->addWidget(label_ebur128_integrated_loudness_title_, 5, 0);
|
||||
layout_play_data_->addWidget(label_ebur128_integrated_loudness_, 5, 1);
|
||||
layout_play_data_->addWidget(label_ebur128_loudness_range_title_, 6, 0);
|
||||
layout_play_data_->addWidget(label_ebur128_loudness_range_, 6, 1);
|
||||
|
||||
widget_play_data_->setLayout(layout_play_data_);
|
||||
|
||||
textedit_play_lyrics_->setReadOnly(true);
|
||||
@@ -218,17 +203,13 @@ ContextView::ContextView(QWidget *parent)
|
||||
<< label_length_title_
|
||||
<< label_samplerate_title_
|
||||
<< label_bitdepth_title_
|
||||
<< label_bitrate_title_
|
||||
<< label_ebur128_integrated_loudness_title_
|
||||
<< label_ebur128_loudness_range_title_;
|
||||
<< label_bitrate_title_;
|
||||
|
||||
labels_play_data_ << label_filetype_
|
||||
<< label_length_
|
||||
<< label_samplerate_
|
||||
<< label_bitdepth_
|
||||
<< label_bitrate_
|
||||
<< label_ebur128_integrated_loudness_
|
||||
<< label_ebur128_loudness_range_;
|
||||
<< label_bitrate_;
|
||||
|
||||
labels_play_all_ = labels_play_ << labels_play_data_;
|
||||
|
||||
@@ -396,7 +377,7 @@ void ContextView::UpdateNoSong() {
|
||||
|
||||
void ContextView::NoSong() {
|
||||
|
||||
if (!widget_album_->isVisible()) {
|
||||
if (!widget_album_->isVisibleTo(this)) {
|
||||
widget_album_->show();
|
||||
}
|
||||
|
||||
@@ -440,11 +421,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;
|
||||
}
|
||||
@@ -493,26 +474,6 @@ void ContextView::SetSong() {
|
||||
label_bitrate_->show();
|
||||
SetLabelText(label_bitrate_, song_playing_.bitrate(), tr("kbps"));
|
||||
}
|
||||
if (!song_playing_.ebur128_integrated_loudness_lufs()) {
|
||||
label_ebur128_integrated_loudness_title_->hide();
|
||||
label_ebur128_integrated_loudness_->hide();
|
||||
label_ebur128_integrated_loudness_->clear();
|
||||
}
|
||||
else {
|
||||
label_ebur128_integrated_loudness_title_->show();
|
||||
label_ebur128_integrated_loudness_->show();
|
||||
label_ebur128_integrated_loudness_->setText(song_playing_.Ebur128LoudnessLUFSToText());
|
||||
}
|
||||
if (!song_playing_.ebur128_loudness_range_lu()) {
|
||||
label_ebur128_loudness_range_title_->hide();
|
||||
label_ebur128_loudness_range_->hide();
|
||||
label_ebur128_loudness_range_->clear();
|
||||
}
|
||||
else {
|
||||
label_ebur128_loudness_range_title_->show();
|
||||
label_ebur128_loudness_range_->show();
|
||||
label_ebur128_loudness_range_->setText(song_playing_.Ebur128LoudnessRangeLUToText());
|
||||
}
|
||||
spacer_play_data_->changeSize(20, 20, QSizePolicy::Fixed);
|
||||
}
|
||||
else {
|
||||
@@ -522,8 +483,6 @@ void ContextView::SetSong() {
|
||||
label_samplerate_->clear();
|
||||
label_bitdepth_->clear();
|
||||
label_bitrate_->clear();
|
||||
label_ebur128_integrated_loudness_->clear();
|
||||
label_ebur128_loudness_range_->clear();
|
||||
spacer_play_data_->changeSize(0, 0, QSizePolicy::Fixed);
|
||||
}
|
||||
|
||||
@@ -598,12 +557,6 @@ void ContextView::UpdateSong(const Song &song) {
|
||||
SetLabelText(label_bitrate_, song.bitrate(), tr("kbps"));
|
||||
}
|
||||
}
|
||||
if (song.ebur128_integrated_loudness_lufs() != song_playing_.ebur128_integrated_loudness_lufs()) {
|
||||
label_ebur128_integrated_loudness_->setText(song_playing_.Ebur128LoudnessLUFSToText());
|
||||
}
|
||||
if (song.ebur128_loudness_range_lu() != song_playing_.ebur128_loudness_range_lu()) {
|
||||
label_ebur128_loudness_range_->setText(song_playing_.Ebur128LoudnessRangeLUToText());
|
||||
}
|
||||
}
|
||||
|
||||
song_playing_ = song;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2013-2022, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2013-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
|
||||
@@ -135,18 +135,12 @@ class ContextView : public QWidget {
|
||||
QLabel *label_bitdepth_title_;
|
||||
QLabel *label_bitrate_title_;
|
||||
|
||||
QLabel *label_ebur128_integrated_loudness_title_;
|
||||
QLabel *label_ebur128_loudness_range_title_;
|
||||
|
||||
QLabel *label_filetype_;
|
||||
QLabel *label_length_;
|
||||
QLabel *label_samplerate_;
|
||||
QLabel *label_bitdepth_;
|
||||
QLabel *label_bitrate_;
|
||||
|
||||
QLabel *label_ebur128_integrated_loudness_;
|
||||
QLabel *label_ebur128_loudness_range_;
|
||||
|
||||
Song song_playing_;
|
||||
Song song_prev_;
|
||||
QImage image_original_;
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
|
||||
#include "scrobbler/audioscrobbler.h"
|
||||
#include "scrobbler/lastfmscrobbler.h"
|
||||
#include "scrobbler/librefmscrobbler.h"
|
||||
#include "scrobbler/listenbrainzscrobbler.h"
|
||||
#include "scrobbler/lastfmimport.h"
|
||||
#ifdef HAVE_SUBSONIC
|
||||
@@ -206,7 +205,6 @@ class ApplicationImpl {
|
||||
scrobbler_([app]() {
|
||||
AudioScrobbler *scrobbler = new AudioScrobbler(app);
|
||||
scrobbler->AddService(make_shared<LastFMScrobbler>(scrobbler->settings(), app->network()));
|
||||
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->network(), app->streaming_services()->Service<SubsonicService>(), app));
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
const int Database::kSchemaVersion = 20;
|
||||
const int Database::kSchemaVersion = 21;
|
||||
|
||||
namespace {
|
||||
constexpr char kDatabaseFilename[] = "strawberry.db";
|
||||
@@ -414,11 +414,6 @@ void Database::ExecSongTablesCommands(QSqlDatabase &db, const QStringList &song_
|
||||
// We allow a magic value in the schema files to update all songs tables at once.
|
||||
if (command.contains(QLatin1String(kMagicAllSongsTables))) {
|
||||
for (const QString &table : song_tables) {
|
||||
// Another horrible hack: device songs tables don't have matching _fts tables, so if this command tries to touch one, ignore it.
|
||||
if (table.startsWith("device_"_L1) && command.contains(QLatin1String(kMagicAllSongsTables) + "_fts"_L1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables;
|
||||
QString new_command(command);
|
||||
new_command.replace(QLatin1String(kMagicAllSongsTables), table);
|
||||
|
||||
@@ -157,12 +157,16 @@ void HttpBaseRequest::HandleSSLErrors(const QList<QSslError> &ssl_errors) {
|
||||
HttpBaseRequest::ReplyDataResult HttpBaseRequest::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
if (reply->error() >= 200) {
|
||||
reply->readAll(); // QTBUG-135641
|
||||
}
|
||||
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) {
|
||||
reply->readAll(); // QTBUG-135641
|
||||
return ReplyDataResult(ErrorCode::HttpError, QStringLiteral("Received HTTP code %1").arg(http_status_code));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2013-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2013-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
|
||||
@@ -156,10 +156,8 @@
|
||||
#include "lyrics/lyricsproviders.h"
|
||||
#include "device/devicemanager.h"
|
||||
#include "device/devicestatefiltermodel.h"
|
||||
#ifndef Q_OS_WIN32
|
||||
# include "device/deviceview.h"
|
||||
# include "device/deviceviewcontainer.h"
|
||||
#endif
|
||||
#include "device/deviceview.h"
|
||||
#include "device/deviceviewcontainer.h"
|
||||
#include "transcoder/transcodedialog.h"
|
||||
#include "settings/settingsdialog.h"
|
||||
#include "constants/behavioursettings.h"
|
||||
@@ -175,6 +173,7 @@
|
||||
# include "constants/tidalsettings.h"
|
||||
#endif
|
||||
#ifdef HAVE_SPOTIFY
|
||||
# include "spotify/spotifyservice.h"
|
||||
# include "constants/spotifysettings.h"
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
@@ -280,7 +279,7 @@ constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-
|
||||
#endif // HAVE_QTSPARKLE
|
||||
|
||||
MainWindow::MainWindow(Application *app,
|
||||
SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd,
|
||||
SharedPtr<SystemTrayIcon> systemtrayicon, OSDBase *osd,
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence,
|
||||
#endif
|
||||
@@ -292,11 +291,14 @@ MainWindow::MainWindow(Application *app,
|
||||
thumbbar_(new Windows7ThumbBar(this)),
|
||||
#endif
|
||||
app_(app),
|
||||
tray_icon_(tray_icon),
|
||||
systemtrayicon_(systemtrayicon),
|
||||
osd_(osd),
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord_rich_presence_(discord_rich_presence),
|
||||
#endif
|
||||
error_dialog_([this]() {
|
||||
return new ErrorDialog(this);
|
||||
}),
|
||||
console_([app, this]() {
|
||||
Console *console = new Console(app->database());
|
||||
QObject::connect(console, &Console::Error, this, &MainWindow::ShowErrorDialog);
|
||||
@@ -310,9 +312,7 @@ MainWindow::MainWindow(Application *app,
|
||||
context_view_(new ContextView(this)),
|
||||
collection_view_(new CollectionViewContainer(this)),
|
||||
file_view_(new FileView(this)),
|
||||
#ifndef Q_OS_WIN32
|
||||
device_view_(new DeviceViewContainer(this)),
|
||||
#endif
|
||||
playlist_list_(new PlaylistListContainer(this)),
|
||||
queue_view_(new QueueView(this)),
|
||||
settings_dialog_(std::bind(&MainWindow::CreateSettingsDialog, this)),
|
||||
@@ -375,9 +375,7 @@ MainWindow::MainWindow(Application *app,
|
||||
playlist_move_to_collection_(nullptr),
|
||||
playlist_open_in_browser_(nullptr),
|
||||
playlist_organize_(nullptr),
|
||||
#ifndef Q_OS_WIN32
|
||||
playlist_copy_to_device_(nullptr),
|
||||
#endif
|
||||
playlist_delete_(nullptr),
|
||||
playlist_queue_(nullptr),
|
||||
playlist_queue_play_next_(nullptr),
|
||||
@@ -409,7 +407,11 @@ MainWindow::MainWindow(Application *app,
|
||||
// Initialize the UI
|
||||
ui_->setupUi(this);
|
||||
|
||||
setWindowIcon(IconLoader::Load(u"strawberry"_s));
|
||||
if (QGuiApplication::platformName() != "wayland"_L1) {
|
||||
setWindowIcon(IconLoader::Load(u"strawberry"_s));
|
||||
}
|
||||
|
||||
systemtrayicon_->SetDevicePixelRatioF(devicePixelRatioF());
|
||||
|
||||
QObject::connect(&*app->database(), &Database::Error, this, &MainWindow::ShowErrorDialog);
|
||||
|
||||
@@ -430,9 +432,7 @@ MainWindow::MainWindow(Application *app,
|
||||
ui_->tabs->AddTab(smartplaylists_view_, u"smartplaylists"_s, IconLoader::Load(u"view-media-playlist"_s, true, 0, 32), tr("Smart playlists"));
|
||||
ui_->tabs->AddTab(file_view_, u"files"_s, IconLoader::Load(u"document-open"_s, true, 0, 32), tr("Files"));
|
||||
ui_->tabs->AddTab(radio_view_, u"radios"_s, IconLoader::Load(u"radio"_s, true, 0, 32), tr("Radios"));
|
||||
#ifndef Q_OS_WIN32
|
||||
ui_->tabs->AddTab(device_view_, u"devices"_s, IconLoader::Load(u"device"_s, true, 0, 32), tr("Devices"));
|
||||
#endif
|
||||
#ifdef HAVE_SUBSONIC
|
||||
ui_->tabs->AddTab(subsonic_view_, u"subsonic"_s, IconLoader::Load(u"subsonic"_s, true, 0, 32), tr("Subsonic"));
|
||||
#endif
|
||||
@@ -480,9 +480,7 @@ MainWindow::MainWindow(Application *app,
|
||||
|
||||
collection_view_->view()->setModel(app_->collection()->model()->filter());
|
||||
collection_view_->view()->Init(app->task_manager(), app->tagreader_client(), app->network(), app->albumcover_loader(), app->current_albumcover_loader(), app->cover_providers(), app->lyrics_providers(), app->collection(), app->device_manager(), app->streaming_services());
|
||||
#ifndef Q_OS_WIN32
|
||||
device_view_->view()->Init(app->task_manager(), app->tagreader_client(), app->device_manager(), app->collection_model()->directory_model());
|
||||
#endif
|
||||
playlist_list_->Init(app_->task_manager(), app->tagreader_client(), app_->playlist_manager(), app_->playlist_backend(), app_->device_manager());
|
||||
|
||||
organize_dialog_->SetDestinationModel(app_->collection()->model()->directory_model());
|
||||
@@ -554,9 +552,7 @@ MainWindow::MainWindow(Application *app,
|
||||
QObject::connect(file_view_, &FileView::CopyToCollection, this, &MainWindow::CopyFilesToCollection);
|
||||
QObject::connect(file_view_, &FileView::MoveToCollection, this, &MainWindow::MoveFilesToCollection);
|
||||
QObject::connect(file_view_, &FileView::EditTags, this, &MainWindow::EditFileTags);
|
||||
#ifndef Q_OS_WIN32
|
||||
QObject::connect(file_view_, &FileView::CopyToDevice, this, &MainWindow::CopyFilesToDevice);
|
||||
#endif
|
||||
file_view_->SetTaskManager(app_->task_manager());
|
||||
|
||||
// Action connections
|
||||
@@ -718,10 +714,8 @@ MainWindow::MainWindow(Application *app,
|
||||
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::triggered, this, &MainWindow::SearchCoverAutomatically);
|
||||
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::toggled, this, &MainWindow::ToggleSearchCoverAuto);
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
// Devices connections
|
||||
QObject::connect(device_view_->view(), &DeviceView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
|
||||
#endif
|
||||
|
||||
// Collection filter widget
|
||||
QActionGroup *collection_view_group = new QActionGroup(this);
|
||||
@@ -784,6 +778,9 @@ MainWindow::MainWindow(Application *app,
|
||||
QObject::connect(spotify_view_->songs_collection_view(), &StreamingCollectionView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
|
||||
QObject::connect(spotify_view_->search_view(), &StreamingSearchView::OpenSettingsDialog, this, &MainWindow::OpenServiceSettingsDialog);
|
||||
QObject::connect(spotify_view_->search_view(), &StreamingSearchView::AddToPlaylist, this, &MainWindow::AddToPlaylist);
|
||||
if (SpotifyServicePtr spotifyservice = app_->streaming_services()->Service<SpotifyService>()) {
|
||||
QObject::connect(&*spotifyservice, &SpotifyService::UpdateSpotifyAccessToken, &*app_->player()->engine(), &EngineBase::UpdateSpotifyAccessToken);
|
||||
}
|
||||
#endif
|
||||
|
||||
QObject::connect(radio_view_, &RadioViewContainer::Refresh, &*app_->radio_services(), &RadioServices::RefreshChannels);
|
||||
@@ -824,9 +821,7 @@ MainWindow::MainWindow(Application *app,
|
||||
playlist_organize_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &MainWindow::PlaylistMoveToCollection);
|
||||
playlist_copy_to_collection_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &MainWindow::PlaylistCopyToCollection);
|
||||
playlist_move_to_collection_ = playlist_menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &MainWindow::PlaylistMoveToCollection);
|
||||
#ifndef Q_OS_WIN32
|
||||
playlist_copy_to_device_ = playlist_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &MainWindow::PlaylistCopyToDevice);
|
||||
#endif
|
||||
playlist_delete_ = playlist_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &MainWindow::PlaylistDelete);
|
||||
playlist_menu_->addSeparator();
|
||||
playlistitem_actions_separator_ = playlist_menu_->addSeparator();
|
||||
@@ -845,10 +840,8 @@ MainWindow::MainWindow(Application *app,
|
||||
QObject::connect(ui_->playlist, &PlaylistContainer::UndoRedoActionsChanged, this, &MainWindow::PlaylistUndoRedoChanged);
|
||||
|
||||
QObject::connect(&*app_->device_manager(), &DeviceManager::DeviceError, this, &MainWindow::ShowErrorDialog);
|
||||
#ifndef WIN32
|
||||
QObject::connect(app_->device_manager()->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, playlist_copy_to_device_, &QAction::setDisabled);
|
||||
playlist_copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
|
||||
#endif
|
||||
|
||||
QObject::connect(&*app_->scrobbler()->settings(), &ScrobblerSettingsService::ScrobblingEnabledChanged, this, &MainWindow::ScrobblingEnabledChanged);
|
||||
QObject::connect(&*app_->scrobbler()->settings(), &ScrobblerSettingsService::ScrobbleButtonVisibilityChanged, this, &MainWindow::ScrobbleButtonVisibilityChanged);
|
||||
@@ -858,14 +851,14 @@ MainWindow::MainWindow(Application *app,
|
||||
mac::SetApplicationHandler(this);
|
||||
#endif
|
||||
// Tray icon
|
||||
tray_icon_->SetupMenu(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_stop_after_this_track, ui_->action_next_track, ui_->action_mute, ui_->action_love, ui_->action_quit);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
|
||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
|
||||
systemtrayicon_->SetupMenu(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_stop_after_this_track, ui_->action_next_track, ui_->action_mute, ui_->action_love, ui_->action_quit);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
|
||||
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
|
||||
|
||||
// Windows 7 thumbbar buttons
|
||||
#ifdef Q_OS_WIN32
|
||||
@@ -980,7 +973,7 @@ MainWindow::MainWindow(Application *app,
|
||||
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdateLastPlayed, &*app_->collection_backend(), &CollectionBackend::UpdateLastPlayed);
|
||||
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdatePlayCount, &*app_->collection_backend(), &CollectionBackend::UpdatePlayCount);
|
||||
|
||||
#if !defined(HAVE_AUDIOCD) || defined(Q_OS_WIN32)
|
||||
#if !defined(HAVE_AUDIOCD)
|
||||
ui_->action_open_cd->setEnabled(false);
|
||||
ui_->action_open_cd->setVisible(false);
|
||||
#endif
|
||||
@@ -1043,7 +1036,7 @@ MainWindow::MainWindow(Application *app,
|
||||
show();
|
||||
break;
|
||||
case BehaviourSettings::StartupBehaviour::Hide:
|
||||
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible()) {
|
||||
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible()) {
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
@@ -1056,7 +1049,7 @@ MainWindow::MainWindow(Application *app,
|
||||
was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool();
|
||||
if (was_minimized_) setWindowState(windowState() | Qt::WindowMinimized);
|
||||
|
||||
if (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
|
||||
if (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
|
||||
show();
|
||||
}
|
||||
break;
|
||||
@@ -1168,13 +1161,13 @@ void MainWindow::ReloadSettings() {
|
||||
#ifdef Q_OS_MACOS
|
||||
constexpr bool keeprunning_available = true;
|
||||
#else
|
||||
const bool systemtray_available = tray_icon_->IsSystemTrayAvailable();
|
||||
const bool systemtray_available = systemtrayicon_->IsSystemTrayAvailable();
|
||||
s.beginGroup(BehaviourSettings::kSettingsGroup);
|
||||
const bool showtrayicon = s.value(BehaviourSettings::kShowTrayIcon, systemtray_available).toBool();
|
||||
s.endGroup();
|
||||
const bool keeprunning_available = systemtray_available && showtrayicon;
|
||||
if (systemtray_available) {
|
||||
tray_icon_->setVisible(showtrayicon);
|
||||
systemtrayicon_->setVisible(showtrayicon);
|
||||
}
|
||||
if ((!showtrayicon || !systemtray_available) && !isVisible()) {
|
||||
show();
|
||||
@@ -1199,7 +1192,7 @@ void MainWindow::ReloadSettings() {
|
||||
int iconsize = s.value(AppearanceSettings::kIconSizePlayControlButtons, 32).toInt();
|
||||
s.endGroup();
|
||||
|
||||
tray_icon_->SetTrayiconProgress(trayicon_progress);
|
||||
systemtrayicon_->SetTrayiconProgress(trayicon_progress);
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_ && !taskbar_progress) {
|
||||
@@ -1221,11 +1214,11 @@ void MainWindow::ReloadSettings() {
|
||||
ui_->volume->SetEnabled(volume_control);
|
||||
if (volume_control) {
|
||||
if (!ui_->action_mute->isVisible()) ui_->action_mute->setVisible(true);
|
||||
if (!tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(true);
|
||||
if (!systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(true);
|
||||
}
|
||||
else {
|
||||
if (ui_->action_mute->isVisible()) ui_->action_mute->setVisible(false);
|
||||
if (tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(false);
|
||||
if (systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1377,8 +1370,8 @@ void MainWindow::Exit() {
|
||||
if (app_->player()->GetState() == EngineBase::State::Playing) {
|
||||
app_->player()->Stop();
|
||||
hide();
|
||||
if (tray_icon_->IsSystemTrayAvailable()) {
|
||||
tray_icon_->setVisible(false);
|
||||
if (systemtrayicon_->IsSystemTrayAvailable()) {
|
||||
systemtrayicon_->setVisible(false);
|
||||
}
|
||||
return; // Don't quit the application now: wait for the fadeout finished signal
|
||||
}
|
||||
@@ -1435,7 +1428,7 @@ void MainWindow::MediaStopped() {
|
||||
|
||||
ui_->action_love->setEnabled(false);
|
||||
ui_->button_love->setEnabled(false);
|
||||
tray_icon_->LoveStateChanged(false);
|
||||
systemtrayicon_->LoveStateChanged(false);
|
||||
|
||||
if (track_position_timer_->isActive()) {
|
||||
track_position_timer_->stop();
|
||||
@@ -1444,8 +1437,8 @@ void MainWindow::MediaStopped() {
|
||||
track_slider_timer_->stop();
|
||||
}
|
||||
ui_->track_slider->SetStopped();
|
||||
tray_icon_->SetProgress(0);
|
||||
tray_icon_->SetStopped();
|
||||
systemtrayicon_->SetProgress(0);
|
||||
systemtrayicon_->SetStopped();
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1477,7 +1470,7 @@ void MainWindow::MediaPaused() {
|
||||
track_slider_timer_->start();
|
||||
}
|
||||
|
||||
tray_icon_->SetPaused();
|
||||
systemtrayicon_->SetPaused();
|
||||
|
||||
}
|
||||
|
||||
@@ -1498,7 +1491,7 @@ void MainWindow::MediaPlaying() {
|
||||
}
|
||||
ui_->action_play_pause->setEnabled(enable_play_pause);
|
||||
ui_->track_slider->SetCanSeek(can_seek);
|
||||
tray_icon_->SetPlaying(enable_play_pause);
|
||||
systemtrayicon_->SetPlaying(enable_play_pause);
|
||||
|
||||
if (!track_position_timer_->isActive()) {
|
||||
track_position_timer_->start();
|
||||
@@ -1515,18 +1508,18 @@ 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);
|
||||
systemtrayicon_->LoveStateChanged(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::VolumeChanged(const uint volume) {
|
||||
ui_->action_mute->setChecked(volume == 0);
|
||||
tray_icon_->MuteButtonStateChanged(volume == 0);
|
||||
systemtrayicon_->MuteButtonStateChanged(volume == 0);
|
||||
}
|
||||
|
||||
void MainWindow::SongChanged(const Song &song) {
|
||||
@@ -1536,7 +1529,7 @@ void MainWindow::SongChanged(const Song &song) {
|
||||
song_playing_ = song;
|
||||
song_ = song;
|
||||
setWindowTitle(song.PrettyTitleWithArtist());
|
||||
tray_icon_->SetProgress(0);
|
||||
systemtrayicon_->SetProgress(0);
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1562,9 +1555,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));
|
||||
@@ -1694,17 +1687,6 @@ void MainWindow::StopAfterCurrent() {
|
||||
Q_EMIT StopAfterToggled(app_->playlist_manager()->active()->stop_after_current());
|
||||
}
|
||||
|
||||
void MainWindow::showEvent(QShowEvent *e) {
|
||||
|
||||
if (error_dialog_ && error_dialog_->isVisible() && error_dialog_->isMinimized()) {
|
||||
error_dialog_->raise();
|
||||
error_dialog_->activateWindow();
|
||||
}
|
||||
|
||||
QMainWindow::showEvent(e);
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::hideEvent(QHideEvent *e) {
|
||||
|
||||
// Some window managers don't remember maximized state between
|
||||
@@ -1719,7 +1701,7 @@ void MainWindow::hideEvent(QHideEvent *e) {
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *e) {
|
||||
|
||||
if (!exit_ && (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !keep_running_)) {
|
||||
if (!exit_ && (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !keep_running_)) {
|
||||
Exit();
|
||||
}
|
||||
|
||||
@@ -1727,10 +1709,20 @@ void MainWindow::closeEvent(QCloseEvent *e) {
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::changeEvent(QEvent *e) {
|
||||
|
||||
if (e->type() == QEvent::Show || e->type() == QEvent::WindowStateChange || e->type() == QEvent::WindowActivate) {
|
||||
CheckShowErrorDialog();
|
||||
}
|
||||
|
||||
QMainWindow::changeEvent(e);
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::SetHiddenInTray(const bool hidden) {
|
||||
|
||||
if (hidden && isVisible()) {
|
||||
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible() && keep_running_) {
|
||||
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible() && keep_running_) {
|
||||
close();
|
||||
}
|
||||
else {
|
||||
@@ -1747,6 +1739,7 @@ void MainWindow::SetHiddenInTray(const bool hidden) {
|
||||
else {
|
||||
show();
|
||||
}
|
||||
CheckShowErrorDialog();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1758,8 +1751,8 @@ 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;
|
||||
tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
const qint64 length = app_->player()->GetCurrentItem()->EffectiveMetadata().length_nanosec() / kNsecPerSec;
|
||||
systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1774,12 +1767,12 @@ 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);
|
||||
|
||||
// Update the tray icon every 10 seconds
|
||||
if (position % 10 == 0) tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
if (position % 10 == 0) systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1788,12 +1781,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);
|
||||
}
|
||||
}
|
||||
@@ -1910,7 +1903,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
|
||||
@@ -1989,12 +1982,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;
|
||||
}
|
||||
|
||||
@@ -2032,9 +2025,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
||||
playlist_show_in_collection_->setVisible(false);
|
||||
playlist_copy_to_collection_->setVisible(false);
|
||||
playlist_move_to_collection_->setVisible(false);
|
||||
#ifndef Q_OS_WIN32
|
||||
playlist_copy_to_device_->setVisible(false);
|
||||
#endif
|
||||
playlist_organize_->setVisible(false);
|
||||
playlist_delete_->setVisible(false);
|
||||
|
||||
@@ -2097,7 +2088,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);
|
||||
@@ -2107,9 +2098,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
||||
playlist_move_to_collection_->setVisible(local_songs > 0);
|
||||
}
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
playlist_copy_to_device_->setVisible(local_songs > 0);
|
||||
#endif
|
||||
|
||||
playlist_delete_->setVisible(delete_files_ && local_songs > 0);
|
||||
|
||||
@@ -2189,9 +2178,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);
|
||||
}
|
||||
@@ -2751,7 +2740,6 @@ void MainWindow::MoveFilesToCollection(const QList<QUrl> &urls) {
|
||||
|
||||
void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
|
||||
organize_dialog_->SetCopy(true);
|
||||
if (organize_dialog_->SetUrls(urls)) {
|
||||
@@ -2761,9 +2749,6 @@ void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
|
||||
else {
|
||||
QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device"));
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(urls);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@@ -2823,7 +2808,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);
|
||||
@@ -2839,7 +2824,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) {
|
||||
@@ -2891,8 +2876,6 @@ void MainWindow::PlaylistSkip() {
|
||||
|
||||
void MainWindow::PlaylistCopyToDevice() {
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
|
||||
SongList songs;
|
||||
|
||||
const QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
|
||||
@@ -2917,8 +2900,6 @@ void MainWindow::PlaylistCopyToDevice() {
|
||||
QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device"));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::ChangeCollectionFilterMode(QAction *action) {
|
||||
@@ -3012,6 +2993,14 @@ void MainWindow::ShowErrorDialog(const QString &message) {
|
||||
error_dialog_->ShowMessage(message);
|
||||
}
|
||||
|
||||
void MainWindow::CheckShowErrorDialog() {
|
||||
|
||||
if (isVisible() && !isMinimized() && error_dialog_ && error_dialog_->isVisible() && !error_dialog_->isActiveWindow()) {
|
||||
error_dialog_->ShowDialog();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::CheckFullRescanRevisions() {
|
||||
|
||||
int from = app_->database()->startup_schema_version();
|
||||
@@ -3283,7 +3272,7 @@ void MainWindow::LoveButtonVisibilityChanged(const bool value) {
|
||||
else
|
||||
ui_->widget_love->hide();
|
||||
|
||||
tray_icon_->LoveVisibilityChanged(value);
|
||||
systemtrayicon_->LoveVisibilityChanged(value);
|
||||
|
||||
}
|
||||
|
||||
@@ -3306,7 +3295,7 @@ void MainWindow::Love() {
|
||||
app_->scrobbler()->Love();
|
||||
ui_->button_love->setEnabled(false);
|
||||
ui_->action_love->setEnabled(false);
|
||||
tray_icon_->LoveStateChanged(false);
|
||||
systemtrayicon_->LoveStateChanged(false);
|
||||
|
||||
}
|
||||
|
||||
@@ -3321,10 +3310,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;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2013-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2013-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
|
||||
@@ -73,9 +73,7 @@ class CollectionViewContainer;
|
||||
class CollectionFilter;
|
||||
class AlbumCoverChoiceController;
|
||||
class CommandlineOptions;
|
||||
#ifndef Q_OS_WIN32
|
||||
class DeviceViewContainer;
|
||||
#endif
|
||||
class EditTagDialog;
|
||||
class Equalizer;
|
||||
class ErrorDialog;
|
||||
@@ -113,7 +111,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
|
||||
public:
|
||||
explicit MainWindow(Application *app,
|
||||
SharedPtr<SystemTrayIcon> tray_icon,
|
||||
SharedPtr<SystemTrayIcon> systemtrayicon,
|
||||
OSDBase *osd,
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence,
|
||||
@@ -126,9 +124,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
void CommandlineOptionsReceived(const CommandlineOptions &options);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *e) override;
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
void closeEvent(QCloseEvent *e) override;
|
||||
void changeEvent(QEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
#ifdef Q_OS_WIN32
|
||||
bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;
|
||||
@@ -238,6 +236,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
|
||||
void ShowAboutDialog();
|
||||
void ShowErrorDialog(const QString &message);
|
||||
void CheckShowErrorDialog();
|
||||
void ShowTranscodeDialog();
|
||||
SettingsDialog *CreateSettingsDialog();
|
||||
EditTagDialog *CreateEditTagDialog();
|
||||
@@ -312,11 +311,12 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
#endif
|
||||
|
||||
Application *app_;
|
||||
SharedPtr<SystemTrayIcon> tray_icon_;
|
||||
SharedPtr<SystemTrayIcon> systemtrayicon_;
|
||||
OSDBase *osd_;
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence_;
|
||||
#endif
|
||||
Lazy<ErrorDialog> error_dialog_;
|
||||
Lazy<About> about_dialog_;
|
||||
Lazy<Console> console_;
|
||||
Lazy<EditTagDialog> edit_tag_dialog_;
|
||||
@@ -327,13 +327,10 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
ContextView *context_view_;
|
||||
CollectionViewContainer *collection_view_;
|
||||
FileView *file_view_;
|
||||
#ifndef Q_OS_WIN32
|
||||
DeviceViewContainer *device_view_;
|
||||
#endif
|
||||
PlaylistListContainer *playlist_list_;
|
||||
QueueView *queue_view_;
|
||||
|
||||
Lazy<ErrorDialog> error_dialog_;
|
||||
Lazy<SettingsDialog> settings_dialog_;
|
||||
Lazy<AlbumCoverManager> cover_manager_;
|
||||
SharedPtr<Equalizer> equalizer_;
|
||||
@@ -380,9 +377,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
QAction *playlist_move_to_collection_;
|
||||
QAction *playlist_open_in_browser_;
|
||||
QAction *playlist_organize_;
|
||||
#ifndef Q_OS_WIN32
|
||||
QAction *playlist_copy_to_device_;
|
||||
#endif
|
||||
QAction *playlist_delete_;
|
||||
QAction *playlist_queue_;
|
||||
QAction *playlist_queue_play_next_;
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
<property name="windowTitle">
|
||||
<string>Strawberry Music Player</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../data/icons.qrc">
|
||||
<normaloff>:/icons/128x128/strawberry.png</normaloff>:/icons/128x128/strawberry.png</iconset>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralWidget">
|
||||
<layout class="QVBoxLayout" name="layout_centralWidget">
|
||||
<property name="spacing">
|
||||
@@ -37,7 +33,7 @@
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="sidebar_layout">
|
||||
<layout class="QVBoxLayout" name="layout_left">
|
||||
@@ -77,7 +73,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -102,7 +98,7 @@
|
||||
<item>
|
||||
<widget class="QFrame" name="player_controls">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="layout_player_controls">
|
||||
<property name="spacing">
|
||||
@@ -167,7 +163,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="popupMode">
|
||||
<enum>QToolButton::ToolButtonPopupMode::MenuButtonPopup</enum>
|
||||
<enum>QToolButton::MenuButtonPopup</enum>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
@@ -211,7 +207,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_love">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -237,7 +233,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_buttons">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -260,10 +256,10 @@
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Expanding</enum>
|
||||
<enum>QSizePolicy::Expanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -276,7 +272,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_volume">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -292,7 +288,7 @@
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -326,7 +322,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="status_bar_line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -380,7 +376,7 @@
|
||||
<item>
|
||||
<widget class="QLabel" name="playlist_summary">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -391,7 +387,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -401,7 +397,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -580,7 +576,7 @@
|
||||
<string>Ctrl+Q</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::QuitRole</enum>
|
||||
<enum>QAction::QuitRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_stop_after_this_track">
|
||||
@@ -644,7 +640,7 @@
|
||||
<string>Ctrl+P</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::PreferencesRole</enum>
|
||||
<enum>QAction::PreferencesRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_about_strawberry">
|
||||
@@ -659,7 +655,7 @@
|
||||
<string>F1</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::AboutRole</enum>
|
||||
<enum>QAction::AboutRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_shuffle">
|
||||
@@ -785,7 +781,7 @@
|
||||
<string>About &Qt</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::AboutQtRole</enum>
|
||||
<enum>QAction::AboutQtRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_mute">
|
||||
|
||||
@@ -43,29 +43,29 @@ NetworkAccessManager::NetworkAccessManager(QObject *parent)
|
||||
|
||||
}
|
||||
|
||||
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) {
|
||||
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) {
|
||||
|
||||
QByteArray user_agent;
|
||||
if (request.hasRawHeader("User-Agent")) {
|
||||
user_agent = request.header(QNetworkRequest::UserAgentHeader).toByteArray();
|
||||
if (network_request.hasRawHeader("User-Agent")) {
|
||||
user_agent = network_request.header(QNetworkRequest::UserAgentHeader).toByteArray();
|
||||
}
|
||||
else {
|
||||
user_agent = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
|
||||
}
|
||||
|
||||
QNetworkRequest new_request(request);
|
||||
new_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
new_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
|
||||
QNetworkRequest new_network_request(network_request);
|
||||
new_network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
new_network_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
|
||||
|
||||
if (op == QNetworkAccessManager::PostOperation && !new_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
|
||||
new_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
|
||||
if (op == QNetworkAccessManager::PostOperation && !new_network_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
|
||||
new_network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
|
||||
}
|
||||
|
||||
// Prefer the cache unless the caller has changed the setting already
|
||||
if (request.attribute(QNetworkRequest::CacheLoadControlAttribute).toInt() == QNetworkRequest::PreferNetwork) {
|
||||
new_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
|
||||
if (!network_request.attribute(QNetworkRequest::CacheLoadControlAttribute).isValid()) {
|
||||
new_network_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
|
||||
}
|
||||
|
||||
return QNetworkAccessManager::createRequest(op, new_request, outgoingData);
|
||||
return QNetworkAccessManager::createRequest(op, new_network_request, outgoing_data);
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class NetworkAccessManager : public QNetworkAccessManager {
|
||||
explicit NetworkAccessManager(QObject *parent = nullptr);
|
||||
|
||||
protected:
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override;
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) override;
|
||||
};
|
||||
|
||||
#endif // NETWORKACCESSMANAGER_H
|
||||
|
||||
@@ -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_;
|
||||
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;
|
||||
@@ -554,7 +554,7 @@ 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 qint64 time = QDateTime::currentSecsSinceEpoch() - pause_time_.toSecsSinceEpoch();
|
||||
if (time >= 30) { // Stream URL might be expired.
|
||||
@@ -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(), static_cast<quint64>(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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -68,9 +68,13 @@
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
const QStringList Song::kColumns = QStringList() << u"title"_s
|
||||
<< u"titlesort"_s
|
||||
<< u"album"_s
|
||||
<< u"albumsort"_s
|
||||
<< u"artist"_s
|
||||
<< u"artistsort"_s
|
||||
<< u"albumartist"_s
|
||||
<< u"albumartistsort"_s
|
||||
<< u"track"_s
|
||||
<< u"disc"_s
|
||||
<< u"year"_s
|
||||
@@ -78,7 +82,9 @@ const QStringList Song::kColumns = QStringList() << u"title"_s
|
||||
<< u"genre"_s
|
||||
<< u"compilation"_s
|
||||
<< u"composer"_s
|
||||
<< u"composersort"_s
|
||||
<< u"performer"_s
|
||||
<< u"performersort"_s
|
||||
<< u"grouping"_s
|
||||
<< u"comment"_s
|
||||
<< u"lyrics"_s
|
||||
@@ -126,6 +132,9 @@ const QStringList Song::kColumns = QStringList() << u"title"_s
|
||||
<< u"cue_path"_s
|
||||
|
||||
<< u"rating"_s
|
||||
<< u"bpm"_s
|
||||
<< u"mood"_s
|
||||
<< u"initial_key"_s
|
||||
|
||||
<< u"acoustid_id"_s
|
||||
<< u"acoustid_fingerprint"_s
|
||||
@@ -234,6 +243,7 @@ const QStringList Song::kAcceptedExtensions = QStringList() << u"wav"_s
|
||||
<< u"tta"_s
|
||||
<< u"dsf"_s
|
||||
<< u"dsd"_s
|
||||
<< u"webm"_s
|
||||
<< u"ac3"_s
|
||||
<< u"dts"_s
|
||||
<< u"spc"_s
|
||||
@@ -261,9 +271,13 @@ struct Song::Private : public QSharedData {
|
||||
bool valid_;
|
||||
|
||||
QString title_;
|
||||
QString titlesort_;
|
||||
QString album_;
|
||||
QString albumsort_;
|
||||
QString artist_;
|
||||
QString artistsort_;
|
||||
QString albumartist_;
|
||||
QString albumartistsort_;
|
||||
int track_;
|
||||
int disc_;
|
||||
int year_;
|
||||
@@ -271,7 +285,9 @@ struct Song::Private : public QSharedData {
|
||||
QString genre_;
|
||||
bool compilation_; // From the file tag
|
||||
QString composer_;
|
||||
QString composersort_;
|
||||
QString performer_;
|
||||
QString performersort_;
|
||||
QString grouping_;
|
||||
QString comment_;
|
||||
QString lyrics_;
|
||||
@@ -316,6 +332,9 @@ struct Song::Private : public QSharedData {
|
||||
QString cue_path_; // If the song has a CUE, this contains it's path.
|
||||
|
||||
float rating_; // Database rating, initial rating read from tag.
|
||||
float bpm_;
|
||||
QString mood_;
|
||||
QString initial_key_;
|
||||
|
||||
QString acoustid_id_;
|
||||
QString acoustid_fingerprint_;
|
||||
@@ -337,12 +356,7 @@ struct Song::Private : public QSharedData {
|
||||
bool init_from_file_; // Whether this song was loaded from a file using taglib.
|
||||
bool suspicious_tags_; // Whether our encoding guesser thinks these tags might be incorrectly encoded.
|
||||
|
||||
QString title_sortable_;
|
||||
QString album_sortable_;
|
||||
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.
|
||||
|
||||
};
|
||||
|
||||
@@ -384,6 +398,7 @@ Song::Private::Private(const Source source)
|
||||
art_unset_(false),
|
||||
|
||||
rating_(-1),
|
||||
bpm_(-1),
|
||||
|
||||
init_from_file_(false),
|
||||
suspicious_tags_(false)
|
||||
@@ -411,9 +426,13 @@ int Song::id() const { return d->id_; }
|
||||
bool Song::is_valid() const { return d->valid_; }
|
||||
|
||||
const QString &Song::title() const { return d->title_; }
|
||||
const QString &Song::titlesort() const { return d->titlesort_; }
|
||||
const QString &Song::album() const { return d->album_; }
|
||||
const QString &Song::albumsort() const { return d->albumsort_; }
|
||||
const QString &Song::artist() const { return d->artist_; }
|
||||
const QString &Song::artistsort() const { return d->artistsort_; }
|
||||
const QString &Song::albumartist() const { return d->albumartist_; }
|
||||
const QString &Song::albumartistsort() const { return d->albumartistsort_; }
|
||||
int Song::track() const { return d->track_; }
|
||||
int Song::disc() const { return d->disc_; }
|
||||
int Song::year() const { return d->year_; }
|
||||
@@ -421,7 +440,9 @@ int Song::originalyear() const { return d->originalyear_; }
|
||||
const QString &Song::genre() const { return d->genre_; }
|
||||
bool Song::compilation() const { return d->compilation_; }
|
||||
const QString &Song::composer() const { return d->composer_; }
|
||||
const QString &Song::composersort() const { return d->composersort_; }
|
||||
const QString &Song::performer() const { return d->performer_; }
|
||||
const QString &Song::performersort() const { return d->performersort_; }
|
||||
const QString &Song::grouping() const { return d->grouping_; }
|
||||
const QString &Song::comment() const { return d->comment_; }
|
||||
const QString &Song::lyrics() const { return d->lyrics_; }
|
||||
@@ -468,6 +489,9 @@ bool Song::art_unset() const { return d->art_unset_; }
|
||||
const QString &Song::cue_path() const { return d->cue_path_; }
|
||||
|
||||
float Song::rating() const { return d->rating_; }
|
||||
float Song::bpm() const { return d->bpm_; }
|
||||
const QString &Song::mood() const { return d->mood_; }
|
||||
const QString &Song::initial_key() const { return d->initial_key_; }
|
||||
|
||||
const QString &Song::acoustid_id() const { return d->acoustid_id_; }
|
||||
const QString &Song::acoustid_fingerprint() const { return d->acoustid_fingerprint_; }
|
||||
@@ -511,20 +535,19 @@ QString *Song::mutable_musicbrainz_work_id() { return &d->musicbrainz_work_id_;
|
||||
|
||||
bool Song::init_from_file() const { return d->init_from_file_; }
|
||||
|
||||
const QString &Song::title_sortable() const { return d->title_sortable_; }
|
||||
const QString &Song::album_sortable() const { return d->album_sortable_; }
|
||||
const QString &Song::artist_sortable() const { return d->artist_sortable_; }
|
||||
const QString &Song::albumartist_sortable() const { return d->albumartist_sortable_; }
|
||||
|
||||
const QUrl &Song::stream_url() const { return d->stream_url_; }
|
||||
|
||||
void Song::set_id(const int id) { d->id_ = id; }
|
||||
void Song::set_valid(const bool v) { d->valid_ = v; }
|
||||
|
||||
void Song::set_title(const QString &v) { d->title_sortable_ = sortable(v); d->title_ = v; }
|
||||
void Song::set_album(const QString &v) { d->album_sortable_ = sortable(v); d->album_ = v; }
|
||||
void Song::set_artist(const QString &v) { d->artist_sortable_ = sortable(v); d->artist_ = v; }
|
||||
void Song::set_albumartist(const QString &v) { d->albumartist_sortable_ = sortable(v); d->albumartist_ = v; }
|
||||
void Song::set_title(const QString &v) { d->title_ = v; }
|
||||
void Song::set_titlesort(const QString &v) { d->titlesort_ = v; }
|
||||
void Song::set_album(const QString &v) { d->album_ = v; }
|
||||
void Song::set_albumsort(const QString &v) { d->albumsort_ = v; }
|
||||
void Song::set_artist(const QString &v) { d->artist_ = v; }
|
||||
void Song::set_artistsort(const QString &v) { d->artistsort_ = v; }
|
||||
void Song::set_albumartist(const QString &v) { d->albumartist_ = v; }
|
||||
void Song::set_albumartistsort(const QString &v) { d->albumartistsort_ = v; }
|
||||
void Song::set_track(const int v) { d->track_ = v; }
|
||||
void Song::set_disc(const int v) { d->disc_ = v; }
|
||||
void Song::set_year(const int v) { d->year_ = v; }
|
||||
@@ -532,7 +555,9 @@ void Song::set_originalyear(const int v) { d->originalyear_ = v; }
|
||||
void Song::set_genre(const QString &v) { d->genre_ = v; }
|
||||
void Song::set_compilation(const bool v) { d->compilation_ = v; }
|
||||
void Song::set_composer(const QString &v) { d->composer_ = v; }
|
||||
void Song::set_composersort(const QString &v) { d->composersort_ = v; }
|
||||
void Song::set_performer(const QString &v) { d->performer_ = v; }
|
||||
void Song::set_performersort(const QString &v) { d->performersort_ = v; }
|
||||
void Song::set_grouping(const QString &v) { d->grouping_ = v; }
|
||||
void Song::set_comment(const QString &v) { d->comment_ = v; }
|
||||
void Song::set_lyrics(const QString &v) { d->lyrics_ = v; }
|
||||
@@ -578,6 +603,9 @@ void Song::set_art_unset(const bool v) { d->art_unset_ = v; }
|
||||
void Song::set_cue_path(const QString &v) { d->cue_path_ = v; }
|
||||
|
||||
void Song::set_rating(const float v) { d->rating_ = v; }
|
||||
void Song::set_bpm(const float v) { d->bpm_ = v; }
|
||||
void Song::set_mood(const QString &v) { d->mood_ = v; }
|
||||
void Song::set_initial_key(const QString &v) { d->initial_key_ = v; }
|
||||
|
||||
void Song::set_acoustid_id(const QString &v) { d->acoustid_id_ = v; }
|
||||
void Song::set_acoustid_fingerprint(const QString &v) { d->acoustid_fingerprint_ = v; }
|
||||
@@ -600,40 +628,19 @@ void Song::set_init_from_file(const bool v) { d->init_from_file_ = v; }
|
||||
|
||||
void Song::set_stream_url(const QUrl &v) { d->stream_url_ = v; }
|
||||
|
||||
void Song::set_title(const TagLib::String &v) {
|
||||
|
||||
const QString title = TagLibStringToQString(v);
|
||||
d->title_sortable_ = sortable(title);
|
||||
d->title_ = title;
|
||||
|
||||
}
|
||||
|
||||
void Song::set_album(const TagLib::String &v) {
|
||||
|
||||
const QString album = TagLibStringToQString(v);
|
||||
d->album_sortable_ = sortable(album);
|
||||
d->album_ = album;
|
||||
|
||||
}
|
||||
void Song::set_artist(const TagLib::String &v) {
|
||||
|
||||
const QString artist = TagLibStringToQString(v);
|
||||
d->artist_sortable_ = sortable(artist);
|
||||
d->artist_ = artist;
|
||||
|
||||
}
|
||||
|
||||
void Song::set_albumartist(const TagLib::String &v) {
|
||||
|
||||
const QString albumartist = TagLibStringToQString(v);
|
||||
d->albumartist_sortable_ = sortable(albumartist);
|
||||
d->albumartist_ = albumartist;
|
||||
|
||||
}
|
||||
|
||||
void Song::set_title(const TagLib::String &v) { d->title_ = TagLibStringToQString(v); }
|
||||
void Song::set_titlesort(const TagLib::String &v) { d->titlesort_ = TagLibStringToQString(v); }
|
||||
void Song::set_album(const TagLib::String &v) { d->album_ = TagLibStringToQString(v); }
|
||||
void Song::set_albumsort(const TagLib::String &v) { d->albumsort_ = TagLibStringToQString(v); }
|
||||
void Song::set_artist(const TagLib::String &v) { d->artist_ = TagLibStringToQString(v); }
|
||||
void Song::set_artistsort(const TagLib::String &v) { d->artistsort_ = TagLibStringToQString(v); }
|
||||
void Song::set_albumartist(const TagLib::String &v) { d->albumartist_ = TagLibStringToQString(v); }
|
||||
void Song::set_albumartistsort(const TagLib::String &v) { d->albumartistsort_ = TagLibStringToQString(v); }
|
||||
void Song::set_genre(const TagLib::String &v) { d->genre_ = TagLibStringToQString(v); }
|
||||
void Song::set_composer(const TagLib::String &v) { d->composer_ = TagLibStringToQString(v); }
|
||||
void Song::set_composersort(const TagLib::String &v) { d->composersort_ = TagLibStringToQString(v); }
|
||||
void Song::set_performer(const TagLib::String &v) { d->performer_ = TagLibStringToQString(v); }
|
||||
void Song::set_performersort(const TagLib::String &v) { d->performersort_ = TagLibStringToQString(v); }
|
||||
void Song::set_grouping(const TagLib::String &v) { d->grouping_ = TagLibStringToQString(v); }
|
||||
void Song::set_comment(const TagLib::String &v) { d->comment_ = TagLibStringToQString(v); }
|
||||
void Song::set_lyrics(const TagLib::String &v) { d->lyrics_ = TagLibStringToQString(v); }
|
||||
@@ -652,14 +659,21 @@ void Song::set_musicbrainz_track_id(const TagLib::String &v) { d->musicbrainz_tr
|
||||
void Song::set_musicbrainz_disc_id(const TagLib::String &v) { d->musicbrainz_disc_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); }
|
||||
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'/'); }
|
||||
void Song::set_mood(const TagLib::String &v) { d->mood_ = TagLibStringToQString(v); }
|
||||
void Song::set_initial_key(const TagLib::String &v) { d->initial_key_ = TagLibStringToQString(v); }
|
||||
|
||||
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_titlesort() const { return d->titlesort_.isEmpty() ? d->title_ : d->titlesort_; }
|
||||
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_albumartistsort() const { return !d->albumartistsort_.isEmpty() ? d->albumartistsort_ : !d->albumartist_.isEmpty() ? d->albumartist_ : effective_artistsort(); }
|
||||
const QString &Song::effective_artistsort() const { return d->artistsort_.isEmpty() ? d->artist_ : d->artistsort_; }
|
||||
const QString &Song::effective_album() const { return d->album_.isEmpty() ? d->title_ : d->album_; }
|
||||
const QString &Song::effective_albumsort() const { return d->albumsort_.isEmpty() ? d->album_ : d->albumsort_; }
|
||||
const QString &Song::effective_composersort() const { return d->composersort_.isEmpty() ? d->composer_ : d->composersort_; }
|
||||
const QString &Song::effective_performersort() const { return d->performersort_.isEmpty() ? d->performer_ : d->performersort_; }
|
||||
int Song::effective_originalyear() const { return d->originalyear_ < 0 ? d->year_ : d->originalyear_; }
|
||||
const QString &Song::playlist_albumartist() const { return is_compilation() ? d->albumartist_ : effective_albumartist(); }
|
||||
const QString &Song::playlist_albumartist_sortable() const { return is_compilation() ? d->albumartist_sortable_ : effective_albumartist_sortable(); }
|
||||
const QString &Song::playlist_effective_albumartist() const { return is_compilation() ? d->albumartist_ : effective_albumartist(); }
|
||||
const QString &Song::playlist_effective_albumartistsort() const { return is_compilation() ? (!d->albumartistsort_.isEmpty() ? d->albumartistsort_ : d->albumartist_) : effective_albumartistsort(); }
|
||||
|
||||
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
|
||||
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
|
||||
@@ -782,6 +796,31 @@ bool Song::lyrics_supported() const {
|
||||
return additional_tags_supported() || d->filetype_ == FileType::ASF;
|
||||
}
|
||||
|
||||
bool Song::albumartistsort_supported() const {
|
||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
|
||||
}
|
||||
|
||||
bool Song::albumsort_supported() const {
|
||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
|
||||
}
|
||||
|
||||
bool Song::artistsort_supported() const {
|
||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
|
||||
}
|
||||
|
||||
bool Song::composersort_supported() const {
|
||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
|
||||
}
|
||||
|
||||
bool Song::performersort_supported() const {
|
||||
// Performer sort is a rare custom field even in vorbis comments, no write support in MPEG formats
|
||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis;
|
||||
}
|
||||
|
||||
bool Song::titlesort_supported() const {
|
||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
|
||||
}
|
||||
|
||||
bool Song::save_embedded_cover_supported(const FileType filetype) {
|
||||
|
||||
return filetype == FileType::FLAC ||
|
||||
@@ -794,21 +833,6 @@ bool Song::save_embedded_cover_supported(const FileType filetype) {
|
||||
|
||||
}
|
||||
|
||||
QString Song::sortable(const QString &v) {
|
||||
|
||||
QString copy = v.toLower();
|
||||
|
||||
for (const auto &i : kArticles) {
|
||||
if (copy.startsWith(i)) {
|
||||
qint64 ilen = i.length();
|
||||
return copy.right(copy.length() - ilen) + u", "_s + copy.left(ilen - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return copy;
|
||||
|
||||
}
|
||||
|
||||
int Song::ColumnIndex(const QString &field) {
|
||||
|
||||
return static_cast<int>(kRowIdColumns.indexOf(field));
|
||||
@@ -923,39 +947,63 @@ 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->titlesort_ == other.d->titlesort_ &&
|
||||
d->album_ == other.d->album_ &&
|
||||
d->albumsort_ == other.d->albumsort_ &&
|
||||
d->artist_ == other.d->artist_ &&
|
||||
d->artistsort_ == other.d->artistsort_ &&
|
||||
d->albumartist_ == other.d->albumartist_ &&
|
||||
d->albumartistsort_ == other.d->albumartistsort_ &&
|
||||
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->composersort_ == other.d->composersort_ &&
|
||||
d->performer_ == other.d->performer_ &&
|
||||
d->performersort_ == other.d->performersort_ &&
|
||||
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->bpm_ == other.d->bpm_ &&
|
||||
d->mood_ == other.d->mood_ &&
|
||||
d->initial_key_ == other.d->initial_key_ &&
|
||||
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_;
|
||||
|
||||
}
|
||||
|
||||
@@ -980,42 +1028,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);
|
||||
|
||||
}
|
||||
|
||||
@@ -1139,6 +1215,22 @@ QIcon Song::IconForSource(const Source source) {
|
||||
|
||||
}
|
||||
|
||||
// Convert a source to a music service domain name, for ListenBrainz.
|
||||
// See the "Music service names" note on https://listenbrainz.readthedocs.io/en/latest/users/json.html.
|
||||
|
||||
QString Song::DomainForSource(const Source source) {
|
||||
|
||||
switch (source) {
|
||||
case Song::Source::Tidal: return u"tidal.com"_s;
|
||||
case Song::Source::Qobuz: return u"qobuz.com"_s;
|
||||
case Song::Source::SomaFM: return u"somafm.com"_s;
|
||||
case Song::Source::RadioParadise: return u"radioparadise.com"_s;
|
||||
case Song::Source::Spotify: return u"spotify.com"_s;
|
||||
default: return QString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString Song::TextForFiletype(const FileType filetype) {
|
||||
|
||||
switch (filetype) {
|
||||
@@ -1166,6 +1258,7 @@ QString Song::TextForFiletype(const FileType filetype) {
|
||||
case FileType::CDDA: return u"CDDA"_s;
|
||||
case FileType::SPC: return u"SNES SPC700"_s;
|
||||
case FileType::VGM: return u"VGM"_s;
|
||||
case FileType::ALAC: return u"ALAC"_s;
|
||||
case FileType::Stream: return u"Stream"_s;
|
||||
case FileType::Unknown:
|
||||
default: return QObject::tr("Unknown");
|
||||
@@ -1198,6 +1291,7 @@ QString Song::ExtensionForFiletype(const FileType filetype) {
|
||||
case FileType::IT: return u"it"_s;
|
||||
case FileType::SPC: return u"spc"_s;
|
||||
case FileType::VGM: return u"vgm"_s;
|
||||
case FileType::ALAC: return u"m4a"_s;
|
||||
case FileType::Unknown:
|
||||
default: return u"dat"_s;
|
||||
}
|
||||
@@ -1230,12 +1324,30 @@ QIcon Song::IconForFiletype(const FileType filetype) {
|
||||
case FileType::IT: return IconLoader::Load(u"it"_s);
|
||||
case FileType::CDDA: return IconLoader::Load(u"cd"_s);
|
||||
case FileType::Stream: return IconLoader::Load(u"applications-internet"_s);
|
||||
case FileType::ALAC: return IconLoader::Load(u"alac"_s);
|
||||
case FileType::Unknown:
|
||||
default: return IconLoader::Load(u"edit-delete"_s);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Get a URL usable for sharing this song with another user.
|
||||
// This is only applicable when streaming from a streaming service, since we can't link to local content.
|
||||
// Returns a web URL which points to the current streaming track or live stream, or an empty string if that is not applicable.
|
||||
|
||||
QString Song::ShareURL() const {
|
||||
|
||||
switch (source()) {
|
||||
case Song::Source::Stream:
|
||||
case Song::Source::SomaFM: return url().toString();
|
||||
case Song::Source::Tidal: return "https://tidal.com/track/%1"_L1.arg(song_id());
|
||||
case Song::Source::Qobuz: return "https://open.qobuz.com/track/%1"_L1.arg(song_id());
|
||||
case Song::Source::Spotify: return "https://open.spotify.com/track/%1"_L1.arg(song_id());
|
||||
default: return QString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsFileLossless() const {
|
||||
|
||||
switch (filetype()) {
|
||||
@@ -1250,6 +1362,7 @@ bool Song::IsFileLossless() const {
|
||||
case FileType::TrueAudio:
|
||||
case FileType::PCM:
|
||||
case FileType::CDDA:
|
||||
case FileType::ALAC:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -1279,6 +1392,7 @@ Song::FileType Song::FiletypeByMimetype(const QString &mimetype) {
|
||||
if (mimetype.compare("audio/x-s3m"_L1, Qt::CaseInsensitive) == 0) return FileType::S3M;
|
||||
if (mimetype.compare("audio/x-spc"_L1, Qt::CaseInsensitive) == 0) return FileType::SPC;
|
||||
if (mimetype.compare("audio/x-vgm"_L1, Qt::CaseInsensitive) == 0) return FileType::VGM;
|
||||
if (mimetype.compare("audio/x-alac"_L1, Qt::CaseInsensitive) == 0) return FileType::ALAC;
|
||||
|
||||
return FileType::Unknown;
|
||||
|
||||
@@ -1306,6 +1420,7 @@ Song::FileType Song::FiletypeByDescription(const QString &text) {
|
||||
if (text.compare("Module Music Format (MOD)"_L1, Qt::CaseInsensitive) == 0) return FileType::S3M;
|
||||
if (text.compare("SNES SPC700"_L1, Qt::CaseInsensitive) == 0) return FileType::SPC;
|
||||
if (text.compare("VGM"_L1, Qt::CaseInsensitive) == 0) return FileType::VGM;
|
||||
if (text.compare("Apple Lossless Audio Codec (ALAC)"_L1, Qt::CaseInsensitive) == 0) return FileType::ALAC;
|
||||
|
||||
return FileType::Unknown;
|
||||
|
||||
@@ -1416,9 +1531,13 @@ void Song::InitFromQuery(const QSqlRecord &r, const bool reliable_metadata, cons
|
||||
d->id_ = SqlHelper::ValueToInt(r, ColumnIndex(u"ROWID"_s) + col);
|
||||
|
||||
set_title(SqlHelper::ValueToString(r, ColumnIndex(u"title"_s) + col));
|
||||
set_titlesort(SqlHelper::ValueToString(r, ColumnIndex(u"titlesort"_s) + col));
|
||||
set_album(SqlHelper::ValueToString(r, ColumnIndex(u"album"_s) + col));
|
||||
set_albumsort(SqlHelper::ValueToString(r, ColumnIndex(u"albumsort"_s) + col));
|
||||
set_artist(SqlHelper::ValueToString(r, ColumnIndex(u"artist"_s) + col));
|
||||
set_artistsort(SqlHelper::ValueToString(r, ColumnIndex(u"artistsort"_s) + col));
|
||||
set_albumartist(SqlHelper::ValueToString(r, ColumnIndex(u"albumartist"_s) + col));
|
||||
set_albumartistsort(SqlHelper::ValueToString(r, ColumnIndex(u"albumartistsort"_s) + col));
|
||||
d->track_ = SqlHelper::ValueToInt(r, ColumnIndex(u"track"_s) + col);
|
||||
d->disc_ = SqlHelper::ValueToInt(r, ColumnIndex(u"disc"_s) + col);
|
||||
d->year_ = SqlHelper::ValueToInt(r, ColumnIndex(u"year"_s) + col);
|
||||
@@ -1426,7 +1545,9 @@ void Song::InitFromQuery(const QSqlRecord &r, const bool reliable_metadata, cons
|
||||
d->genre_ = SqlHelper::ValueToString(r, ColumnIndex(u"genre"_s) + col);
|
||||
d->compilation_ = r.value(ColumnIndex(u"compilation"_s) + col).toBool();
|
||||
d->composer_ = SqlHelper::ValueToString(r, ColumnIndex(u"composer"_s) + col);
|
||||
d->composersort_ = SqlHelper::ValueToString(r, ColumnIndex(u"composersort"_s) + col);
|
||||
d->performer_ = SqlHelper::ValueToString(r, ColumnIndex(u"performer"_s) + col);
|
||||
d->performersort_ = SqlHelper::ValueToString(r, ColumnIndex(u"performersort"_s) + col);
|
||||
d->grouping_ = SqlHelper::ValueToString(r, ColumnIndex(u"grouping"_s) + col);
|
||||
d->comment_ = SqlHelper::ValueToString(r, ColumnIndex(u"comment"_s) + col);
|
||||
d->lyrics_ = SqlHelper::ValueToString(r, ColumnIndex(u"lyrics"_s) + col);
|
||||
@@ -1468,7 +1589,11 @@ void Song::InitFromQuery(const QSqlRecord &r, const bool reliable_metadata, cons
|
||||
d->art_unset_ = SqlHelper::ValueToBool(r, ColumnIndex(u"art_unset"_s) + col);
|
||||
|
||||
d->cue_path_ = SqlHelper::ValueToString(r, ColumnIndex(u"cue_path"_s) + col);
|
||||
|
||||
d->rating_ = SqlHelper::ValueToFloat(r, ColumnIndex(u"rating"_s) + col);
|
||||
d->bpm_ = SqlHelper::ValueToFloat(r, ColumnIndex(u"bpm"_s) + col);
|
||||
d->mood_ = SqlHelper::ValueToString(r, ColumnIndex(u"mood"_s) + col);
|
||||
d->initial_key_ = SqlHelper::ValueToString(r, ColumnIndex(u"initial_key"_s) + col);
|
||||
|
||||
d->acoustid_id_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_id"_s) + col);
|
||||
d->acoustid_fingerprint_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_fingerprint"_s) + col);
|
||||
@@ -1734,9 +1859,13 @@ void Song::BindToQuery(SqlQuery *query) const {
|
||||
// Remember to bind these in the same order as kBindSpec
|
||||
|
||||
query->BindStringValue(u":title"_s, d->title_);
|
||||
query->BindStringValue(u":titlesort"_s, d->titlesort_);
|
||||
query->BindStringValue(u":album"_s, d->album_);
|
||||
query->BindStringValue(u":albumsort"_s, d->albumsort_);
|
||||
query->BindStringValue(u":artist"_s, d->artist_);
|
||||
query->BindStringValue(u":artistsort"_s, d->artistsort_);
|
||||
query->BindStringValue(u":albumartist"_s, d->albumartist_);
|
||||
query->BindStringValue(u":albumartistsort"_s, d->albumartistsort_);
|
||||
query->BindIntValue(u":track"_s, d->track_);
|
||||
query->BindIntValue(u":disc"_s, d->disc_);
|
||||
query->BindIntValue(u":year"_s, d->year_);
|
||||
@@ -1744,7 +1873,9 @@ void Song::BindToQuery(SqlQuery *query) const {
|
||||
query->BindStringValue(u":genre"_s, d->genre_);
|
||||
query->BindBoolValue(u":compilation"_s, d->compilation_);
|
||||
query->BindStringValue(u":composer"_s, d->composer_);
|
||||
query->BindStringValue(u":composersort"_s, d->composersort_);
|
||||
query->BindStringValue(u":performer"_s, d->performer_);
|
||||
query->BindStringValue(u":performersort"_s, d->performersort_);
|
||||
query->BindStringValue(u":grouping"_s, d->grouping_);
|
||||
query->BindStringValue(u":comment"_s, d->comment_);
|
||||
query->BindStringValue(u":lyrics"_s, d->lyrics_);
|
||||
@@ -1792,6 +1923,9 @@ void Song::BindToQuery(SqlQuery *query) const {
|
||||
query->BindValue(u":cue_path"_s, d->cue_path_);
|
||||
|
||||
query->BindFloatValue(u":rating"_s, d->rating_);
|
||||
query->BindFloatValue(u":bpm"_s, d->bpm_);
|
||||
query->BindStringValue(u":mood"_s, d->mood_);
|
||||
query->BindStringValue(u":initial_key"_s, d->initial_key_);
|
||||
|
||||
query->BindStringValue(u":acoustid_id"_s, d->acoustid_id_);
|
||||
query->BindStringValue(u":acoustid_fingerprint"_s, d->acoustid_fingerprint_);
|
||||
@@ -1819,7 +1953,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);
|
||||
|
||||
@@ -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
|
||||
@@ -105,6 +105,7 @@ class Song {
|
||||
IT = 21,
|
||||
SPC = 22,
|
||||
VGM = 23,
|
||||
ALAC = 24, // MP4, with ALAC codec
|
||||
CDDA = 90,
|
||||
Stream = 91
|
||||
};
|
||||
@@ -149,9 +150,13 @@ class Song {
|
||||
bool is_valid() const;
|
||||
|
||||
const QString &title() const;
|
||||
const QString &titlesort() const;
|
||||
const QString &album() const;
|
||||
const QString &albumsort() const;
|
||||
const QString &artist() const;
|
||||
const QString &artistsort() const;
|
||||
const QString &albumartist() const;
|
||||
const QString &albumartistsort() const;
|
||||
int track() const;
|
||||
int disc() const;
|
||||
int year() const;
|
||||
@@ -159,7 +164,9 @@ class Song {
|
||||
const QString &genre() const;
|
||||
bool compilation() const;
|
||||
const QString &composer() const;
|
||||
const QString &composersort() const;
|
||||
const QString &performer() const;
|
||||
const QString &performersort() const;
|
||||
const QString &grouping() const;
|
||||
const QString &comment() const;
|
||||
const QString &lyrics() const;
|
||||
@@ -206,6 +213,9 @@ class Song {
|
||||
const QString &cue_path() const;
|
||||
|
||||
float rating() const;
|
||||
float bpm() const;
|
||||
const QString &mood() const;
|
||||
const QString &initial_key() const;
|
||||
|
||||
const QString &acoustid_id() const;
|
||||
const QString &acoustid_fingerprint() const;
|
||||
@@ -249,11 +259,6 @@ class Song {
|
||||
|
||||
bool init_from_file() const;
|
||||
|
||||
const QString &title_sortable() const;
|
||||
const QString &album_sortable() const;
|
||||
const QString &artist_sortable() const;
|
||||
const QString &albumartist_sortable() const;
|
||||
|
||||
const QUrl &stream_url() const;
|
||||
|
||||
// Setters
|
||||
@@ -261,9 +266,13 @@ class Song {
|
||||
void set_valid(const bool v);
|
||||
|
||||
void set_title(const QString &v);
|
||||
void set_titlesort(const QString &v);
|
||||
void set_album(const QString &v);
|
||||
void set_albumsort(const QString &v);
|
||||
void set_artist(const QString &v);
|
||||
void set_artistsort(const QString &v);
|
||||
void set_albumartist(const QString &v);
|
||||
void set_albumartistsort(const QString &v);
|
||||
void set_track(const int v);
|
||||
void set_disc(const int v);
|
||||
void set_year(const int v);
|
||||
@@ -271,7 +280,9 @@ class Song {
|
||||
void set_genre(const QString &v);
|
||||
void set_compilation(bool v);
|
||||
void set_composer(const QString &v);
|
||||
void set_composersort(const QString &v);
|
||||
void set_performer(const QString &v);
|
||||
void set_performersort(const QString &v);
|
||||
void set_grouping(const QString &v);
|
||||
void set_comment(const QString &v);
|
||||
void set_lyrics(const QString &v);
|
||||
@@ -317,6 +328,9 @@ class Song {
|
||||
void set_cue_path(const QString &v);
|
||||
|
||||
void set_rating(const float v);
|
||||
void set_bpm(const float v);
|
||||
void set_mood(const QString &v);
|
||||
void set_initial_key(const QString &v);
|
||||
|
||||
void set_acoustid_id(const QString &v);
|
||||
void set_acoustid_fingerprint(const QString &v);
|
||||
@@ -340,12 +354,18 @@ class Song {
|
||||
void set_stream_url(const QUrl &v);
|
||||
|
||||
void set_title(const TagLib::String &v);
|
||||
void set_titlesort(const TagLib::String &v);
|
||||
void set_album(const TagLib::String &v);
|
||||
void set_albumsort(const TagLib::String &v);
|
||||
void set_artist(const TagLib::String &v);
|
||||
void set_artistsort(const TagLib::String &v);
|
||||
void set_albumartist(const TagLib::String &v);
|
||||
void set_albumartistsort(const TagLib::String &v);
|
||||
void set_genre(const TagLib::String &v);
|
||||
void set_composer(const TagLib::String &v);
|
||||
void set_composersort(const TagLib::String &v);
|
||||
void set_performer(const TagLib::String &v);
|
||||
void set_performersort(const TagLib::String &v);
|
||||
void set_grouping(const TagLib::String &v);
|
||||
void set_comment(const TagLib::String &v);
|
||||
void set_lyrics(const TagLib::String &v);
|
||||
@@ -364,14 +384,21 @@ class Song {
|
||||
void set_musicbrainz_disc_id(const TagLib::String &v);
|
||||
void set_musicbrainz_release_group_id(const TagLib::String &v);
|
||||
void set_musicbrainz_work_id(const TagLib::String &v);
|
||||
void set_mood(const TagLib::String &v);
|
||||
void set_initial_key(const TagLib::String &v);
|
||||
|
||||
const QUrl &effective_stream_url() const;
|
||||
const QUrl &effective_url() const;
|
||||
const QString &effective_titlesort() const;
|
||||
const QString &effective_albumartist() const;
|
||||
const QString &effective_albumartist_sortable() const;
|
||||
const QString &effective_albumartistsort() const;
|
||||
const QString &effective_artistsort() const;
|
||||
const QString &effective_album() const;
|
||||
const QString &effective_albumsort() const;
|
||||
const QString &effective_composersort() const;
|
||||
const QString &effective_performersort() const;
|
||||
int effective_originalyear() const;
|
||||
const QString &playlist_albumartist() const;
|
||||
const QString &playlist_albumartist_sortable() const;
|
||||
const QString &playlist_effective_albumartist() const;
|
||||
const QString &playlist_effective_albumartistsort() const;
|
||||
|
||||
bool is_metadata_good() const;
|
||||
bool is_local_collection_song() const;
|
||||
@@ -402,6 +429,13 @@ class Song {
|
||||
bool comment_supported() const;
|
||||
bool lyrics_supported() const;
|
||||
|
||||
bool albumartistsort_supported() const;
|
||||
bool albumsort_supported() const;
|
||||
bool artistsort_supported() const;
|
||||
bool composersort_supported() const;
|
||||
bool performersort_supported() const;
|
||||
bool titlesort_supported() const;
|
||||
|
||||
static bool save_embedded_cover_supported(const FileType filetype);
|
||||
bool save_embedded_cover_supported() const { return url().isLocalFile() && save_embedded_cover_supported(filetype()) && !has_cue(); };
|
||||
|
||||
@@ -430,6 +464,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;
|
||||
@@ -438,7 +473,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;
|
||||
@@ -448,6 +486,7 @@ class Song {
|
||||
static QString DescriptionForSource(const Source source);
|
||||
static Source SourceFromText(const QString &source);
|
||||
static QIcon IconForSource(const Source source);
|
||||
static QString DomainForSource(const Source source);
|
||||
static QString TextForFiletype(const FileType filetype);
|
||||
static QString ExtensionForFiletype(const FileType filetype);
|
||||
static QIcon IconForFiletype(const FileType filetype);
|
||||
@@ -455,9 +494,12 @@ class Song {
|
||||
QString TextForSource() const { return TextForSource(source()); }
|
||||
QString DescriptionForSource() const { return DescriptionForSource(source()); }
|
||||
QIcon IconForSource() const { return IconForSource(source()); }
|
||||
QString DomainForSource() const { return DomainForSource(source()); }
|
||||
QString TextForFiletype() const { return TextForFiletype(filetype()); }
|
||||
QIcon IconForFiletype() const { return IconForFiletype(filetype()); }
|
||||
|
||||
QString ShareURL() const;
|
||||
|
||||
bool IsFileLossless() const;
|
||||
static FileType FiletypeByMimetype(const QString &mimetype);
|
||||
static FileType FiletypeByDescription(const QString &text);
|
||||
@@ -521,9 +563,6 @@ class Song {
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
|
||||
static QString sortable(const QString &v);
|
||||
|
||||
QSharedDataPointer<Private> d;
|
||||
};
|
||||
|
||||
|
||||
@@ -178,9 +178,11 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
|
||||
SongLoader::Result SongLoader::LoadAudioCD() {
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
CddaSongLoader *cdda_song_loader = new CddaSongLoader(QUrl(), this);
|
||||
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsDurationLoaded, this, &SongLoader::AudioCDTracksLoadFinishedSlot);
|
||||
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsMetadataLoaded, this, &SongLoader::AudioCDTracksTagsLoaded);
|
||||
CDDASongLoader *cdda_song_loader = new CDDASongLoader(QUrl(), this);
|
||||
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadError, this, &SongLoader::AudioCDTracksLoadErrorSlot);
|
||||
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsLoaded, this, &SongLoader::AudioCDTracksLoadedSlot);
|
||||
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsUpdated, this, &SongLoader::AudioCDTracksUpdatedSlot);
|
||||
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadingFinished, this, &SongLoader::AudioCDLoadingFinishedSlot);
|
||||
cdda_song_loader->LoadSongs();
|
||||
return Result::Success;
|
||||
#else
|
||||
@@ -192,23 +194,38 @@ SongLoader::Result SongLoader::LoadAudioCD() {
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
|
||||
void SongLoader::AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error) {
|
||||
void SongLoader::AudioCDTracksLoadErrorSlot(const QString &error) {
|
||||
|
||||
songs_ = songs;
|
||||
errors_ << error;
|
||||
Q_EMIT AudioCDTracksLoadFinished();
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::AudioCDTracksTagsLoaded(const SongList &songs) {
|
||||
void SongLoader::AudioCDTracksLoadedSlot(const SongList &songs) {
|
||||
|
||||
CddaSongLoader *cdda_song_loader = qobject_cast<CddaSongLoader*>(sender());
|
||||
cdda_song_loader->deleteLater();
|
||||
songs_ = songs;
|
||||
Q_EMIT LoadAudioCDFinished(true);
|
||||
|
||||
Q_EMIT AudioCDTracksLoaded();
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
void SongLoader::AudioCDTracksUpdatedSlot(const SongList &songs) {
|
||||
|
||||
songs_ = songs;
|
||||
|
||||
Q_EMIT AudioCDTracksUpdated();
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::AudioCDLoadingFinishedSlot() {
|
||||
|
||||
CDDASongLoader *cdda_song_loader = qobject_cast<CDDASongLoader*>(sender());
|
||||
cdda_song_loader->deleteLater();
|
||||
|
||||
Q_EMIT AudioCDLoadingFinished(true);
|
||||
|
||||
}
|
||||
|
||||
#endif // HAVE_AUDIOCD
|
||||
|
||||
SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class ParserBase;
|
||||
class CueParser;
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
class CddaSongLoader;
|
||||
class CDDASongLoader;
|
||||
#endif
|
||||
|
||||
class SongLoader : public QObject {
|
||||
@@ -90,17 +90,21 @@ class SongLoader : public QObject {
|
||||
QStringList errors() { return errors_; }
|
||||
|
||||
Q_SIGNALS:
|
||||
void AudioCDTracksLoadFinished();
|
||||
void LoadAudioCDFinished(const bool success);
|
||||
void AudioCDTracksLoaded();
|
||||
void AudioCDTracksUpdated();
|
||||
void AudioCDLoadingFinished(const bool success);
|
||||
void LoadRemoteFinished();
|
||||
|
||||
private Q_SLOTS:
|
||||
void ScheduleTimeout();
|
||||
void Timeout();
|
||||
void StopTypefind();
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
void AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error);
|
||||
void AudioCDTracksTagsLoaded(const SongList &songs);
|
||||
void AudioCDTracksLoadErrorSlot(const QString &error);
|
||||
void AudioCDTracksLoadedSlot(const SongList &songs);
|
||||
void AudioCDTracksUpdatedSlot(const SongList &songs);
|
||||
void AudioCDLoadingFinishedSlot();
|
||||
#endif // HAVE_AUDIOCD
|
||||
|
||||
private:
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
#include <QTextStream>
|
||||
#include <QFile>
|
||||
#include <QString>
|
||||
#include <QColor>
|
||||
#include <QPalette>
|
||||
#include <QColor>
|
||||
#include <QEvent>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
@@ -80,20 +80,13 @@ void StyleSheetLoader::UpdateStyleSheet(QWidget *widget, SharedPtr<StyleSheetDat
|
||||
// Replace %palette-role with actual colours
|
||||
QPalette p(widget->palette());
|
||||
|
||||
{
|
||||
QColor alt = p.color(QPalette::AlternateBase);
|
||||
QColor color_altbase = p.color(QPalette::AlternateBase);
|
||||
#ifdef Q_OS_MACOS
|
||||
if (alt.lightness() > 180) {
|
||||
alt.setAlpha(130);
|
||||
}
|
||||
else {
|
||||
alt.setAlpha(16);
|
||||
}
|
||||
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? (color_altbase.lightness() > 180 ? 130 : 16) : color_altbase.alpha());
|
||||
#else
|
||||
alt.setAlpha(130);
|
||||
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? 116 : color_altbase.alpha());
|
||||
#endif
|
||||
stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(alt.red()).arg(alt.green()).arg(alt.blue()).arg(alt.alpha()));
|
||||
}
|
||||
stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(color_altbase.red()).arg(color_altbase.green()).arg(color_altbase.blue()).arg(color_altbase.alpha()));
|
||||
|
||||
ReplaceColor(&stylesheet, u"Window"_s, p, QPalette::Window);
|
||||
ReplaceColor(&stylesheet, u"Background"_s, p, QPalette::Window);
|
||||
|
||||
@@ -98,7 +98,7 @@ 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->authentication_required() && !provider->authenticated()) {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class CoverProvider : public JsonBaseRequest {
|
||||
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);
|
||||
|
||||
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_; }
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -158,16 +158,16 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
}
|
||||
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
if (!json_object.isEmpty()) {
|
||||
if (json_object.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array_data;
|
||||
if (json_object.contains("data"_L1) && json_object["DATA"_L1].isArray()) {
|
||||
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();
|
||||
array_data = json_object["DATA"_L1].toArray();
|
||||
}
|
||||
else {
|
||||
Error(u"Json reply object is missing data."_s, json_object);
|
||||
@@ -180,23 +180,23 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
|
||||
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;
|
||||
}
|
||||
const QJsonObject value_object = json_value.toObject();
|
||||
const QJsonObject object_entry = value_entry.toObject();
|
||||
QJsonObject object_album;
|
||||
if (value_object.contains("album"_L1) && value_object["album"_L1].isObject()) { // Song search, so extract the album.
|
||||
object_album = value_object["album"_L1].toObject();
|
||||
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 {
|
||||
object_album = value_object;
|
||||
object_album = object_entry;
|
||||
}
|
||||
|
||||
if (!value_object.contains("id"_L1) || !object_album.contains("id"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value object is missing ID."_s, value_object);
|
||||
if (!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;
|
||||
}
|
||||
|
||||
@@ -210,11 +210,11 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!json_object.contains("artist"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value object is missing artist."_s, json_object);
|
||||
if (!object_entry.contains("artist"_L1)) {
|
||||
Error(u"Invalid Json reply, data array value object is missing artist."_s, object_entry);
|
||||
continue;
|
||||
}
|
||||
const QJsonValue value_artist = json_object["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;
|
||||
@@ -242,12 +242,12 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
<< qMakePair(u"cover_big"_s, QSize(500, 500));
|
||||
for (const QPair<QString, QSize> &cover_size : cover_sizes) {
|
||||
if (!object_album.contains(cover_size.first)) continue;
|
||||
QString cover = object_album[cover_size.first].toString();
|
||||
const QString cover = object_album[cover_size.first].toString();
|
||||
if (!have_cover) {
|
||||
have_cover = true;
|
||||
++i;
|
||||
}
|
||||
QUrl url(cover);
|
||||
const QUrl url(cover);
|
||||
if (!cover_results.contains(url)) {
|
||||
cover_result.image_url = url;
|
||||
cover_result.image_size = cover_size.second;
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
#include "core/oauthenticator.h"
|
||||
#include "albumcoverfetcher.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
@@ -50,8 +49,9 @@ 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;
|
||||
constexpr const int kRequestsDelay = 1000;
|
||||
constexpr char kContentTypeHeader[] = "application/vnd.api+json";
|
||||
constexpr int kSearchLimit = 6;
|
||||
constexpr const int kRequestsDelay = 300;
|
||||
} // namespace
|
||||
|
||||
using std::make_shared;
|
||||
@@ -87,8 +87,7 @@ bool OpenTidalCoverProvider::StartSearch(const QString &artist, const QString &a
|
||||
return false;
|
||||
}
|
||||
|
||||
SearchRequestPtr search_request = make_shared<SearchRequest>(id, artist, album, title);
|
||||
search_requests_queue_ << search_request;
|
||||
search_requests_queue_.enqueue(make_shared<QueuedSearchRequest>(make_shared<SearchRequest>(id, artist, album, title)));
|
||||
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
@@ -109,8 +108,21 @@ void OpenTidalCoverProvider::FlushRequests() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!artwork_requests_queue_.isEmpty()) {
|
||||
QueuedArtworkRequestPtr queued_artwork_request = artwork_requests_queue_.dequeue();
|
||||
SendArtworkRequest(queued_artwork_request->search, queued_artwork_request->albumcover, queued_artwork_request->artwork);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!albumcover_requests_queue_.isEmpty()) {
|
||||
QueuedAlbumCoverRequestPtr queued_albumcover_request = albumcover_requests_queue_.dequeue();
|
||||
SendAlbumCoverRequest(queued_albumcover_request->search, queued_albumcover_request->albumcover);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!search_requests_queue_.isEmpty()) {
|
||||
SendSearchRequest(search_requests_queue_.dequeue());
|
||||
QueuedSearchRequestPtr queued_search_request = search_requests_queue_.dequeue();
|
||||
SendSearchRequest(queued_search_request->search);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -233,14 +245,14 @@ void OpenTidalCoverProvider::SendSearchRequest(SearchRequestPtr search_request)
|
||||
}
|
||||
|
||||
QUrlQuery url_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) + "/searchresults/"_L1 + QString::fromUtf8(QUrl::toPercentEncoding(query)));
|
||||
url_query.addQueryItem(u"limit"_s, QString::number(kSearchLimit));
|
||||
url_query.addQueryItem(u"include"_s, u"albums"_s);
|
||||
QUrl url(QLatin1String(kApiUrl) + "/searchResults/"_L1 + QString::fromUtf8(QUrl::toPercentEncoding(query)));
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest network_request(url);
|
||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/vnd.tidal.v1+json"_s);
|
||||
network_request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(kContentTypeHeader));
|
||||
if (oauth_->authenticated()) {
|
||||
network_request.setRawHeader("Authorization", oauth_->authorization_header());
|
||||
}
|
||||
@@ -258,95 +270,247 @@ void OpenTidalCoverProvider::HandleSearchReply(QNetworkReply *reply, SearchReque
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
CoverProviderSearchResults results;
|
||||
const QScopeGuard search_finished = qScopeGuard([this, search_request, &results]() { Q_EMIT SearchFinished(search_request->id, results); });
|
||||
const QScopeGuard search_finished = qScopeGuard([this, search_request]() {
|
||||
if (!search_request->finished && search_request->albumcover_requests.isEmpty()) {
|
||||
Q_EMIT SearchFinished(search_request->id, search_request->results);
|
||||
search_request->finished = true;
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
search_requests_queue_.prepend(make_shared<QueuedSearchRequest>(search_request));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
const QJsonArray array_included = json_object["included"_L1].toArray();
|
||||
if (array_included.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &value : array_included) {
|
||||
if (!value.isObject()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonObject object = value.toObject();
|
||||
const QString id = object["id"_L1].toString();
|
||||
const QString type = object["type"_L1].toString();
|
||||
if (type == "albums"_L1) {
|
||||
QString title;
|
||||
if (object.contains("attributes"_L1)) {
|
||||
const QJsonObject attributes = object["attributes"_L1].toObject();
|
||||
if (attributes.contains("title"_L1)) {
|
||||
title = attributes["title"_L1].toString();
|
||||
}
|
||||
}
|
||||
AddAlbumCoverRequest(search_request, id, title);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::AddAlbumCoverRequest(SearchRequestPtr search_request, const QString &album_id, const QString &album_title) {
|
||||
|
||||
AlbumCoverRequestPtr albumcover_request = make_shared<AlbumCoverRequest>(album_id, album_title);
|
||||
search_request->albumcover_requests << albumcover_request;
|
||||
albumcover_requests_queue_.enqueue(make_shared<QueuedAlbumCoverRequest>(search_request, albumcover_request));
|
||||
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::SendAlbumCoverRequest(SearchRequestPtr search_request, AlbumCoverRequestPtr albumcover_request) {
|
||||
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem(u"countryCode"_s, u"US"_s);
|
||||
QUrl url(QLatin1String(kApiUrl) + QLatin1String("/albums/%1/relationships/coverArt"_L1).arg(albumcover_request->album_id));
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest network_request(url);
|
||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
network_request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(kContentTypeHeader));
|
||||
if (oauth_->authenticated()) {
|
||||
network_request.setRawHeader("Authorization", oauth_->authorization_header());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = network_->get(network_request);
|
||||
replies_ << reply;
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, search_request, albumcover_request]() { HandleAlbumCoverReply(reply, search_request, albumcover_request); });
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::HandleAlbumCoverReply(QNetworkReply *reply, SearchRequestPtr search_request, AlbumCoverRequestPtr albumcover_request) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
const QScopeGuard search_finished = qScopeGuard([this, search_request, albumcover_request]() {
|
||||
if (albumcover_request->artwork_requests.isEmpty()) {
|
||||
search_request->albumcover_requests.removeAll(albumcover_request);
|
||||
}
|
||||
if (!search_request->finished && search_request->albumcover_requests.isEmpty()) {
|
||||
Q_EMIT SearchFinished(search_request->id, search_request->results);
|
||||
search_request->finished = true;
|
||||
}
|
||||
});
|
||||
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
if (login_in_progress_) {
|
||||
albumcover_requests_queue_.prepend(make_shared<QueuedAlbumCoverRequest>(search_request, albumcover_request));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
if (!json_object.contains("data"_L1) || !json_object["data"_L1].isArray()) {
|
||||
return;
|
||||
}
|
||||
const QJsonArray array_data = json_object["data"_L1].toArray();
|
||||
if (array_data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &value : array_data) {
|
||||
if (!value.isObject()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonObject object = value.toObject();
|
||||
if (!object.contains("id"_L1) || !object.contains("type"_L1)) {
|
||||
continue;
|
||||
}
|
||||
const QString id = object["id"_L1].toString();
|
||||
const QString type = object["type"_L1].toString();
|
||||
if (type == "artworks"_L1) {
|
||||
AddArtworkRequest(search_request, albumcover_request, id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::AddArtworkRequest(SearchRequestPtr search_request, AlbumCoverRequestPtr albumcover_request, const QString &artwork_id) {
|
||||
|
||||
ArtworkRequestPtr artwork_request = make_shared<ArtworkRequest>(artwork_id);
|
||||
albumcover_request->artwork_requests << artwork_request;
|
||||
artwork_requests_queue_.enqueue(make_shared<QueuedArtworkRequest>(search_request, albumcover_request, artwork_request));
|
||||
|
||||
if (!timer_flush_requests_->isActive()) {
|
||||
timer_flush_requests_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::SendArtworkRequest(SearchRequestPtr search_request, AlbumCoverRequestPtr albumcover_request, ArtworkRequestPtr artwork_request) {
|
||||
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem(u"countryCode"_s, u"US"_s);
|
||||
QUrl url(QLatin1String(kApiUrl) + QLatin1String("/artworks/%1").arg(artwork_request->artwork_id));
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest network_request(url);
|
||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
network_request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(kContentTypeHeader));
|
||||
if (oauth_->authenticated()) {
|
||||
network_request.setRawHeader("Authorization", oauth_->authorization_header());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = network_->get(network_request);
|
||||
replies_ << reply;
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, search_request, albumcover_request, artwork_request]() { HandleArtworkReply(reply, search_request, albumcover_request, artwork_request); });
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::HandleArtworkReply(QNetworkReply *reply, SearchRequestPtr search_request, AlbumCoverRequestPtr albumcover_request, ArtworkRequestPtr artwork_request) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
const QScopeGuard search_finished = qScopeGuard([this, search_request, albumcover_request, artwork_request]() {
|
||||
albumcover_request->artwork_requests.removeAll(artwork_request);
|
||||
if (albumcover_request->artwork_requests.isEmpty()) {
|
||||
search_request->albumcover_requests.removeAll(albumcover_request);
|
||||
}
|
||||
if (!search_request->finished && search_request->albumcover_requests.isEmpty()) {
|
||||
Q_EMIT SearchFinished(search_request->id, search_request->results);
|
||||
search_request->finished = true;
|
||||
}
|
||||
});
|
||||
|
||||
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||
if (!json_object_result.success()) {
|
||||
Error(json_object_result.error_message);
|
||||
if (login_in_progress_) {
|
||||
artwork_requests_queue_.prepend(make_shared<QueuedArtworkRequest>(search_request, albumcover_request, artwork_request));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const QJsonObject &json_object = json_object_result.json_object;
|
||||
if (!json_object.contains("data"_L1) || !json_object["data"_L1].isObject()) {
|
||||
return;
|
||||
}
|
||||
const QJsonObject object_data = json_object["data"_L1].toObject();
|
||||
if (!object_data.contains("attributes"_L1) || !object_data["attributes"_L1].isObject()) {
|
||||
return;
|
||||
}
|
||||
const QJsonObject object_attributes = object_data["attributes"_L1].toObject();
|
||||
if (!object_attributes.contains("files"_L1) || !object_attributes["files"_L1].isArray()) {
|
||||
return;
|
||||
}
|
||||
const QJsonArray array_files = object_attributes["files"_L1].toArray();
|
||||
int i = 0;
|
||||
for (const auto &value_included : array_included) {
|
||||
|
||||
if (!value_included.isObject()) {
|
||||
qLog(Error) << "OpenTidal: Invalid Json reply: Albums array value is not a object.";
|
||||
for (const auto &value_file : array_files) {
|
||||
if (!value_file.isObject()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonObject object_included = value_included.toObject();
|
||||
|
||||
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;
|
||||
const QJsonObject object_file = value_file.toObject();
|
||||
if (!object_file.contains("href"_L1) || !object_file["href"_L1].isString()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonObject object_attributes = object_included["attributes"_L1].toObject();
|
||||
|
||||
if (!object_attributes.contains("title"_L1) || !object_attributes["title"_L1].isString()) {
|
||||
qLog(Error) << "OpenTidal: Invalid Json reply: Attributes is missing title string." << object_attributes;
|
||||
if (!object_file.contains("meta"_L1) || !object_file["meta"_L1].isObject()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!object_attributes.contains("imageLinks"_L1) || !object_attributes["imageLinks"_L1].isArray()) {
|
||||
qLog(Error) << "OpenTidal: Invalid Json reply: Attributes is missing imageLinks object." << object_attributes;
|
||||
const QString href = object_file["href"_L1].toString();
|
||||
const QJsonObject object_meta = object_file["meta"_L1].toObject();
|
||||
if (!object_meta.contains("width"_L1) || !object_meta.contains("height"_L1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString album = object_attributes["title"_L1].toString();
|
||||
const QJsonArray array_imagelinks = object_attributes["imageLinks"_L1].toArray();
|
||||
|
||||
for (const auto &value_imagelink : array_imagelinks) {
|
||||
if (!value_imagelink.isObject()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonObject object_imagelink = value_imagelink.toObject();
|
||||
if (!object_imagelink.contains("href"_L1) || !object_imagelink.contains("meta"_L1) || !object_imagelink["meta"_L1].isObject()) {
|
||||
continue;
|
||||
}
|
||||
QJsonObject object_meta = object_imagelink["meta"_L1].toObject();
|
||||
if (!object_meta.contains("width"_L1) || !object_meta.contains("height"_L1)) {
|
||||
continue;
|
||||
}
|
||||
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 = search_request->artist;
|
||||
cover_result.album = Song::AlbumRemoveDiscMisc(album);
|
||||
cover_result.image_url = url;
|
||||
cover_result.image_size = QSize(width, height);
|
||||
cover_result.number = ++i;
|
||||
results << cover_result;
|
||||
}
|
||||
const int width = object_meta["width"_L1].toInt();
|
||||
const int height = object_meta["height"_L1].toInt();
|
||||
const QUrl url(href);
|
||||
if (!url.isValid() || width < 640 || height < 640) continue;
|
||||
CoverProviderSearchResult cover_result;
|
||||
cover_result.artist = search_request->artist;
|
||||
cover_result.album = albumcover_request->album_title;
|
||||
cover_result.image_url = url;
|
||||
cover_result.image_size = QSize(width, height);
|
||||
cover_result.number = ++i;
|
||||
search_request->results << cover_result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::FinishAllSearches() {
|
||||
|
||||
timer_flush_requests_->stop();
|
||||
|
||||
while (!search_requests_queue_.isEmpty()) {
|
||||
SearchRequestPtr search_request = search_requests_queue_.dequeue();
|
||||
Q_EMIT SearchFinished(search_request->id, CoverProviderSearchResults());
|
||||
QueuedSearchRequestPtr queued_search_request = search_requests_queue_.dequeue();
|
||||
SearchRequestPtr search_request = queued_search_request->search;
|
||||
search_request->albumcover_requests.clear();
|
||||
if (!search_request->finished) {
|
||||
Q_EMIT SearchFinished(search_request->id, CoverProviderSearchResults());
|
||||
search_request->finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
timer_flush_requests_->stop();
|
||||
|
||||
}
|
||||
|
||||
void OpenTidalCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "jsoncoverprovider.h"
|
||||
|
||||
class QTimer;
|
||||
class QNetworkReply;
|
||||
class NetworkAccessManager;
|
||||
class QTimer;
|
||||
class OAuthenticator;
|
||||
|
||||
class OpenTidalCoverProvider : public JsonCoverProvider {
|
||||
@@ -45,20 +45,68 @@ class OpenTidalCoverProvider : public JsonCoverProvider {
|
||||
void CancelSearch(const int id) override;
|
||||
|
||||
private:
|
||||
struct SearchRequest {
|
||||
explicit SearchRequest(const int _id, const QString &_artist, const QString &_album, const QString &_title) : id(_id), artist(_artist), album(_album), title(_title) {}
|
||||
class ArtworkRequest {
|
||||
public:
|
||||
explicit ArtworkRequest(const QString &_artwork_id) : artwork_id(_artwork_id) {}
|
||||
QString artwork_id;
|
||||
};
|
||||
using ArtworkRequestPtr = SharedPtr<ArtworkRequest>;
|
||||
|
||||
class AlbumCoverRequest {
|
||||
public:
|
||||
explicit AlbumCoverRequest(const QString &_album_id, const QString &_album_title) : album_id(_album_id), album_title(_album_title) {}
|
||||
QString album_id;
|
||||
QString album_title;
|
||||
QList<ArtworkRequestPtr> artwork_requests;
|
||||
};
|
||||
using AlbumCoverRequestPtr = SharedPtr<AlbumCoverRequest>;
|
||||
|
||||
class SearchRequest {
|
||||
public:
|
||||
explicit SearchRequest(const int _id, const QString &_artist, const QString &_album, const QString &_title) : id(_id), artist(_artist), album(_album), title(_title), finished(false) {}
|
||||
int id;
|
||||
QString artist;
|
||||
QString album;
|
||||
QString title;
|
||||
QList<AlbumCoverRequestPtr> albumcover_requests;
|
||||
CoverProviderSearchResults results;
|
||||
bool finished;
|
||||
};
|
||||
using SearchRequestPtr = SharedPtr<SearchRequest>;
|
||||
|
||||
class QueuedSearchRequest {
|
||||
public:
|
||||
explicit QueuedSearchRequest(SearchRequestPtr _search) : search(_search) {}
|
||||
SearchRequestPtr search;
|
||||
};
|
||||
using QueuedSearchRequestPtr = SharedPtr<QueuedSearchRequest>;
|
||||
|
||||
class QueuedAlbumCoverRequest {
|
||||
public:
|
||||
explicit QueuedAlbumCoverRequest(SearchRequestPtr _search, AlbumCoverRequestPtr _albumcover) : search(_search), albumcover(_albumcover) {}
|
||||
SearchRequestPtr search;
|
||||
AlbumCoverRequestPtr albumcover;
|
||||
};
|
||||
using QueuedAlbumCoverRequestPtr = SharedPtr<QueuedAlbumCoverRequest>;
|
||||
|
||||
class QueuedArtworkRequest {
|
||||
public:
|
||||
explicit QueuedArtworkRequest(SearchRequestPtr _search, AlbumCoverRequestPtr _albumcover, ArtworkRequestPtr _artwork) : search(_search), albumcover(_albumcover), artwork(_artwork) {}
|
||||
SearchRequestPtr search;
|
||||
AlbumCoverRequestPtr albumcover;
|
||||
ArtworkRequestPtr artwork;
|
||||
};
|
||||
using QueuedArtworkRequestPtr = SharedPtr<QueuedArtworkRequest>;
|
||||
|
||||
private:
|
||||
void LoginCheck();
|
||||
void Login();
|
||||
void SendSearchRequest(SearchRequestPtr request);
|
||||
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
|
||||
void SendSearchRequest(SearchRequestPtr request);
|
||||
void AddAlbumCoverRequest(SearchRequestPtr search_request, const QString &album_id, const QString &album_title);
|
||||
void SendAlbumCoverRequest(SearchRequestPtr search_request, AlbumCoverRequestPtr albumcover_request);
|
||||
void AddArtworkRequest(SearchRequestPtr search_request, AlbumCoverRequestPtr albumcover_request, const QString &artwork_id);
|
||||
void SendArtworkRequest(SearchRequestPtr search_request, AlbumCoverRequestPtr albumcover_request, ArtworkRequestPtr artwork_request);
|
||||
void FinishAllSearches();
|
||||
void Error(const QString &error, const QVariant &debug = QVariant()) override;
|
||||
|
||||
@@ -66,13 +114,17 @@ class OpenTidalCoverProvider : public JsonCoverProvider {
|
||||
void OAuthFinished(const bool success, const QString &error = QString());
|
||||
void FlushRequests();
|
||||
void HandleSearchReply(QNetworkReply *reply, OpenTidalCoverProvider::SearchRequestPtr search_request);
|
||||
void HandleAlbumCoverReply(QNetworkReply *reply, SearchRequestPtr search_request, AlbumCoverRequestPtr albumcover_request);
|
||||
void HandleArtworkReply(QNetworkReply *reply, SearchRequestPtr search_request, AlbumCoverRequestPtr albumcover_request, ArtworkRequestPtr artwork_request);
|
||||
|
||||
private:
|
||||
OAuthenticator *oauth_;
|
||||
QTimer *timer_flush_requests_;
|
||||
bool login_in_progress_;
|
||||
QDateTime last_login_attempt_;
|
||||
QQueue<SearchRequestPtr> search_requests_queue_;
|
||||
QQueue<QueuedSearchRequestPtr> search_requests_queue_;
|
||||
QQueue<QueuedAlbumCoverRequestPtr> albumcover_requests_queue_;
|
||||
QQueue<QueuedArtworkRequestPtr> artwork_requests_queue_;
|
||||
};
|
||||
|
||||
#endif // OPENTIDALCOVERPROVIDER_H
|
||||
|
||||
@@ -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,19 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include <cdio/types.h>
|
||||
#include <cdio/cdio.h>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QTimer>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/logging.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "cddasongloader.h"
|
||||
#include "connecteddevice.h"
|
||||
@@ -33,7 +42,9 @@
|
||||
class DeviceLister;
|
||||
class DeviceManager;
|
||||
|
||||
CddaDevice::CddaDevice(const QUrl &url,
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
CDDADevice::CDDADevice(const QUrl &url,
|
||||
DeviceLister *lister,
|
||||
const QString &unique_id,
|
||||
DeviceManager *device_manager,
|
||||
@@ -45,36 +56,86 @@ CddaDevice::CddaDevice(const QUrl &url,
|
||||
const bool first_time,
|
||||
QObject *parent)
|
||||
: ConnectedDevice(url, lister, unique_id, device_manager, task_manager, database, tagreader_client, albumcover_loader, database_id, first_time, parent),
|
||||
cdda_song_loader_(url) {
|
||||
cdda_song_loader_(url),
|
||||
cdio_(nullptr),
|
||||
timer_disc_changed_(new QTimer(this)) {
|
||||
|
||||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsLoaded, this, &CddaDevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsDurationLoaded, this, &CddaDevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsMetadataLoaded, this, &CddaDevice::SongsLoaded);
|
||||
QObject::connect(this, &CddaDevice::SongsDiscovered, collection_model_, &CollectionModel::AddReAddOrUpdate);
|
||||
timer_disc_changed_->setInterval(1s);
|
||||
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsLoaded, this, &CDDADevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsUpdated, this, &CDDADevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::LoadingFinished, this, &CDDADevice::SongLoadingFinished);
|
||||
QObject::connect(this, &CDDADevice::SongsDiscovered, collection_model_, &CollectionModel::AddReAddOrUpdate);
|
||||
QObject::connect(timer_disc_changed_, &QTimer::timeout, this, &CDDADevice::CheckDiscChanged);
|
||||
|
||||
}
|
||||
|
||||
bool CddaDevice::Init() {
|
||||
CDDADevice::~CDDADevice() {
|
||||
|
||||
if (cdio_) {
|
||||
cdio_destroy(cdio_);
|
||||
cdio_ = nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CDDADevice::Init() {
|
||||
|
||||
if (!cdio_) {
|
||||
cdio_ = cdio_open(url_.path().toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
if (!cdio_) return false;
|
||||
}
|
||||
|
||||
LoadSongs();
|
||||
|
||||
WatchForDiscChanges(true);
|
||||
|
||||
song_count_ = 0; // Reset song count, in case it was already set
|
||||
cdda_song_loader_.LoadSongs();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void CddaDevice::Refresh() {
|
||||
void CDDADevice::WatchForDiscChanges(const bool watch) {
|
||||
|
||||
if (!cdda_song_loader_.HasChanged()) {
|
||||
return;
|
||||
if (watch && !timer_disc_changed_->isActive()) {
|
||||
timer_disc_changed_->start();
|
||||
}
|
||||
else if (!watch && timer_disc_changed_->isActive()) {
|
||||
timer_disc_changed_->stop();
|
||||
}
|
||||
Init();
|
||||
|
||||
}
|
||||
|
||||
void CddaDevice::SongsLoaded(const SongList &songs) {
|
||||
void CDDADevice::CheckDiscChanged() {
|
||||
|
||||
if (!cdio_ || cdda_song_loader_.IsActive()) return;
|
||||
|
||||
if (cdio_get_media_changed(cdio_) == 1) {
|
||||
qLog(Debug) << "CD changed, reloading songs";
|
||||
SongsLoaded();
|
||||
LoadSongs();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CDDADevice::LoadSongs() {
|
||||
|
||||
cdda_song_loader_.LoadSongs();
|
||||
WatchForDiscChanges(false);
|
||||
|
||||
}
|
||||
|
||||
void CDDADevice::SongsLoaded(const SongList &songs) {
|
||||
|
||||
collection_model_->Reset();
|
||||
Q_EMIT SongsDiscovered(songs);
|
||||
song_count_ = songs.size();
|
||||
(void)cdio_get_media_changed(cdio_);
|
||||
|
||||
}
|
||||
|
||||
void CDDADevice::SongLoadingFinished() {
|
||||
|
||||
WatchForDiscChanges(true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +24,11 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include <cdio/types.h>
|
||||
#include <cdio/cdio.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
@@ -35,6 +40,8 @@
|
||||
#include "cddasongloader.h"
|
||||
#include "connecteddevice.h"
|
||||
|
||||
class QTimer;
|
||||
|
||||
class DeviceLister;
|
||||
class DeviceManager;
|
||||
class TaskManager;
|
||||
@@ -42,11 +49,11 @@ class Database;
|
||||
class TagReaderClient;
|
||||
class AlbumCoverLoader;
|
||||
|
||||
class CddaDevice : public ConnectedDevice {
|
||||
class CDDADevice : public ConnectedDevice {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE explicit CddaDevice(const QUrl &url,
|
||||
Q_INVOKABLE explicit CDDADevice(const QUrl &url,
|
||||
DeviceLister *lister,
|
||||
const QString &unique_id,
|
||||
DeviceManager *device_manager,
|
||||
@@ -58,21 +65,29 @@ class CddaDevice : public ConnectedDevice {
|
||||
const bool first_time,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
~CDDADevice();
|
||||
|
||||
bool Init() override;
|
||||
void Refresh() override;
|
||||
bool CopyToStorage(const CopyJob&, QString&) override { return false; }
|
||||
bool DeleteFromStorage(const MusicStorage::DeleteJob&) override { return false; }
|
||||
|
||||
static QStringList url_schemes() { return QStringList() << QStringLiteral("cdda"); }
|
||||
|
||||
void LoadSongs();
|
||||
void WatchForDiscChanges(const bool watch);
|
||||
|
||||
Q_SIGNALS:
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
|
||||
private Q_SLOTS:
|
||||
void SongsLoaded(const SongList &songs);
|
||||
void CheckDiscChanged();
|
||||
void SongsLoaded(const SongList &songs = SongList());
|
||||
void SongLoadingFinished();
|
||||
|
||||
private:
|
||||
CddaSongLoader cdda_song_loader_;
|
||||
CDDASongLoader cdda_song_loader_;
|
||||
CdIo_t *cdio_;
|
||||
QTimer *timer_disc_changed_;
|
||||
};
|
||||
|
||||
#endif // CDDADEVICE_H
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
QStringList CddaLister::DeviceUniqueIDs() { return devices_list_; }
|
||||
QStringList CDDALister::DeviceUniqueIDs() { return devices_list_; }
|
||||
|
||||
QVariantList CddaLister::DeviceIcons(const QString &id) {
|
||||
QVariantList CDDALister::DeviceIcons(const QString &id) {
|
||||
|
||||
Q_UNUSED(id)
|
||||
|
||||
@@ -52,7 +52,7 @@ QVariantList CddaLister::DeviceIcons(const QString &id) {
|
||||
|
||||
}
|
||||
|
||||
QString CddaLister::DeviceManufacturer(const QString &id) {
|
||||
QString CDDALister::DeviceManufacturer(const QString &id) {
|
||||
|
||||
CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
cdio_hwinfo_t cd_info;
|
||||
@@ -65,7 +65,7 @@ QString CddaLister::DeviceManufacturer(const QString &id) {
|
||||
|
||||
}
|
||||
|
||||
QString CddaLister::DeviceModel(const QString &id) {
|
||||
QString CDDALister::DeviceModel(const QString &id) {
|
||||
|
||||
CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
cdio_hwinfo_t cd_info;
|
||||
@@ -78,7 +78,7 @@ QString CddaLister::DeviceModel(const QString &id) {
|
||||
|
||||
}
|
||||
|
||||
quint64 CddaLister::DeviceCapacity(const QString &id) {
|
||||
quint64 CDDALister::DeviceCapacity(const QString &id) {
|
||||
|
||||
Q_UNUSED(id)
|
||||
|
||||
@@ -86,7 +86,7 @@ quint64 CddaLister::DeviceCapacity(const QString &id) {
|
||||
|
||||
}
|
||||
|
||||
quint64 CddaLister::DeviceFreeSpace(const QString &id) {
|
||||
quint64 CDDALister::DeviceFreeSpace(const QString &id) {
|
||||
|
||||
Q_UNUSED(id)
|
||||
|
||||
@@ -94,37 +94,38 @@ quint64 CddaLister::DeviceFreeSpace(const QString &id) {
|
||||
|
||||
}
|
||||
|
||||
QVariantMap CddaLister::DeviceHardwareInfo(const QString &id) {
|
||||
QVariantMap CDDALister::DeviceHardwareInfo(const QString &id) {
|
||||
Q_UNUSED(id)
|
||||
return QVariantMap();
|
||||
}
|
||||
|
||||
QString CddaLister::MakeFriendlyName(const QString &id) {
|
||||
QString CDDALister::MakeFriendlyName(const QString &id) {
|
||||
|
||||
CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
cdio_hwinfo_t cd_info;
|
||||
if (cdio_get_hwinfo(cdio, &cd_info)) {
|
||||
const QString friendly_name = QString::fromUtf8(cd_info.psz_model).trimmed();
|
||||
cdio_destroy(cdio);
|
||||
return QString::fromUtf8(cd_info.psz_model);
|
||||
return friendly_name;
|
||||
}
|
||||
cdio_destroy(cdio);
|
||||
return u"CD ("_s + id + QLatin1Char(')');
|
||||
|
||||
}
|
||||
|
||||
QList<QUrl> CddaLister::MakeDeviceUrls(const QString &id) {
|
||||
QList<QUrl> CDDALister::MakeDeviceUrls(const QString &id) {
|
||||
return QList<QUrl>() << QUrl(u"cdda://"_s + id);
|
||||
}
|
||||
|
||||
void CddaLister::UnmountDevice(const QString &id) {
|
||||
void CDDALister::UnmountDevice(const QString &id) {
|
||||
cdio_eject_media_drive(id.toLocal8Bit().constData());
|
||||
}
|
||||
|
||||
void CddaLister::UpdateDeviceFreeSpace(const QString &id) {
|
||||
void CDDALister::UpdateDeviceFreeSpace(const QString &id) {
|
||||
Q_UNUSED(id)
|
||||
}
|
||||
|
||||
bool CddaLister::Init() {
|
||||
bool CDDALister::Init() {
|
||||
|
||||
cdio_init();
|
||||
#ifdef Q_OS_MACOS
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
|
||||
#include "devicelister.h"
|
||||
|
||||
class CddaLister : public DeviceLister {
|
||||
class CDDALister : public DeviceLister {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CddaLister(QObject *parent = nullptr) : DeviceLister(parent) {}
|
||||
explicit CDDALister(QObject *parent = nullptr) : DeviceLister(parent) {}
|
||||
|
||||
QStringList DeviceUniqueIDs() override;
|
||||
QVariantList DeviceIcons(const QString &id) override;
|
||||
|
||||
@@ -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,25 +21,24 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
|
||||
#include <glib.h>
|
||||
#include <glib/gtypes.h>
|
||||
#include <glib-object.h>
|
||||
|
||||
#include <cdio/cdio.h>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/tag/tag.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "cddasongloader.h"
|
||||
#include "includes/shared_ptr.h"
|
||||
@@ -51,18 +50,27 @@ using std::make_shared;
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
CddaSongLoader::CddaSongLoader(const QUrl &url, QObject *parent)
|
||||
CDDASongLoader::CDDASongLoader(const QUrl &url, QObject *parent)
|
||||
: QObject(parent),
|
||||
url_(url),
|
||||
network_(make_shared<NetworkAccessManager>()),
|
||||
cdda_(nullptr),
|
||||
cdio_(nullptr) {}
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
musicbrainz_client_(new MusicBrainzClient(network_, this)),
|
||||
#endif
|
||||
whatever_(false) {
|
||||
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
QObject::connect(this, &CDDASongLoader::LoadTagsFromMusicBrainz, this, &CDDASongLoader::LoadTagsFromMusicBrainzSlot);
|
||||
QObject::connect(musicbrainz_client_, &MusicBrainzClient::DiscIdFinished, this, &CDDASongLoader::LoadTagsFromMusicBrainzFinished);
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
|
||||
CddaSongLoader::~CddaSongLoader() {
|
||||
if (cdio_) cdio_destroy(cdio_);
|
||||
}
|
||||
|
||||
QUrl CddaSongLoader::GetUrlFromTrack(int track_number) const {
|
||||
CDDASongLoader::~CDDASongLoader() {
|
||||
loading_future_.waitForFinished();
|
||||
}
|
||||
|
||||
QUrl CDDASongLoader::GetUrlFromTrack(int track_number) const {
|
||||
|
||||
if (url_.isEmpty()) {
|
||||
return QUrl(QStringLiteral("cdda://%1a").arg(track_number));
|
||||
@@ -72,72 +80,77 @@ QUrl CddaSongLoader::GetUrlFromTrack(int track_number) const {
|
||||
|
||||
}
|
||||
|
||||
void CddaSongLoader::LoadSongs() {
|
||||
void CDDASongLoader::LoadSongs() {
|
||||
|
||||
QMutexLocker locker(&mutex_load_);
|
||||
cdio_ = cdio_open(url_.path().toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
if (cdio_ == nullptr) {
|
||||
Error(u"Unable to open CDIO device."_s);
|
||||
if (IsActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create gstreamer cdda element
|
||||
loading_future_ = QtConcurrent::run(&CDDASongLoader::LoadSongsFromCDDA, this);
|
||||
|
||||
}
|
||||
|
||||
void CDDASongLoader::LoadSongsFromCDDA() {
|
||||
|
||||
QMutexLocker l(&mutex_load_);
|
||||
|
||||
GError *error = nullptr;
|
||||
cdda_ = gst_element_make_from_uri(GST_URI_SRC, "cdda://", nullptr, &error);
|
||||
GstElement *cdda = gst_element_factory_make("cdiocddasrc", nullptr);
|
||||
if (error) {
|
||||
Error(QStringLiteral("%1: %2").arg(error->code).arg(QString::fromUtf8(error->message)));
|
||||
}
|
||||
if (!cdda_) return;
|
||||
if (!cdda) {
|
||||
Error(tr("Could not create cdiocddasrc"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url_.isEmpty()) {
|
||||
g_object_set(cdda_, "device", g_strdup(url_.path().toLocal8Bit().constData()), nullptr);
|
||||
g_object_set(cdda, "device", g_strdup(url_.path().toLocal8Bit().constData()), nullptr);
|
||||
}
|
||||
if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda_), "paranoia-mode")) {
|
||||
g_object_set(cdda_, "paranoia-mode", 0, nullptr);
|
||||
if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda), "paranoia-mode")) {
|
||||
g_object_set(cdda, "paranoia-mode", 0, nullptr);
|
||||
}
|
||||
|
||||
// Change the element's state to ready and paused, to be able to query it
|
||||
if (gst_element_set_state(cdda_, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) {
|
||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
cdda_ = nullptr;
|
||||
if (gst_element_set_state(cdda, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) {
|
||||
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while setting CDDA device to ready state."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (gst_element_set_state(cdda_, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
|
||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
cdda_ = nullptr;
|
||||
if (gst_element_set_state(cdda, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
|
||||
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while setting CDDA device to pause state."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get number of tracks
|
||||
GstFormat fmt = gst_format_get_by_nick("track");
|
||||
GstFormat out_fmt = fmt;
|
||||
gint64 num_tracks = 0;
|
||||
if (!gst_element_query_duration(cdda_, out_fmt, &num_tracks)) {
|
||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
cdda_ = nullptr;
|
||||
GstFormat format_track = gst_format_get_by_nick("track");
|
||||
GstFormat format_duration = format_track;
|
||||
gint64 total_tracks = 0;
|
||||
if (!gst_element_query_duration(cdda, format_duration, &total_tracks)) {
|
||||
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while querying CDDA tracks."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (out_fmt != fmt) {
|
||||
qLog(Error) << "Error while querying cdda GstElement (2).";
|
||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
cdda_ = nullptr;
|
||||
if (format_duration != format_track) {
|
||||
qLog(Error) << "Error while querying CDDA GstElement (2).";
|
||||
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while querying CDDA tracks."));
|
||||
return;
|
||||
}
|
||||
|
||||
SongList songs;
|
||||
songs.reserve(num_tracks);
|
||||
for (int track_number = 1; track_number <= num_tracks; ++track_number) {
|
||||
// Init song
|
||||
QMap<int, Song> songs;
|
||||
for (int track_number = 1; track_number <= total_tracks; ++track_number) {
|
||||
Song song(Song::Source::CDDA);
|
||||
song.set_id(track_number);
|
||||
song.set_valid(true);
|
||||
@@ -145,129 +158,279 @@ void CddaSongLoader::LoadSongs() {
|
||||
song.set_url(GetUrlFromTrack(track_number));
|
||||
song.set_title(QStringLiteral("Track %1").arg(track_number));
|
||||
song.set_track(track_number);
|
||||
songs << song;
|
||||
songs.insert(track_number, song);
|
||||
}
|
||||
Q_EMIT SongsLoaded(songs);
|
||||
|
||||
Q_EMIT SongsLoaded(songs.values());
|
||||
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
gst_tag_register_musicbrainz_tags();
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
|
||||
GstElement *pipeline = gst_pipeline_new("pipeline");
|
||||
GstElement *sink = gst_element_factory_make("fakesink", nullptr);
|
||||
gst_bin_add_many(GST_BIN(pipeline), cdda_, sink, nullptr);
|
||||
gst_element_link(cdda_, sink);
|
||||
gst_bin_add_many(GST_BIN(pipeline), cdda, sink, nullptr);
|
||||
gst_element_link(cdda, sink);
|
||||
gst_element_set_state(pipeline, GST_STATE_READY);
|
||||
gst_element_set_state(pipeline, GST_STATE_PAUSED);
|
||||
|
||||
// Get TOC and TAG messages
|
||||
GstMessage *msg = nullptr;
|
||||
GstMessage *msg_toc = nullptr;
|
||||
GstMessage *msg_tag = nullptr;
|
||||
while ((msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND, static_cast<GstMessageType>(GST_MESSAGE_TOC | GST_MESSAGE_TAG)))) {
|
||||
if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TOC) {
|
||||
if (msg_toc) gst_message_unref(msg_toc); // Shouldn't happen, but just in case
|
||||
msg_toc = msg;
|
||||
}
|
||||
else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
|
||||
if (msg_tag) gst_message_unref(msg_tag);
|
||||
msg_tag = msg;
|
||||
}
|
||||
}
|
||||
int track_artist_tags = 0;
|
||||
int track_album_tags = 0;
|
||||
int track_title_tags = 0;
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
QString musicbrainz_discid;
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
GstMessageType msg_filter = static_cast<GstMessageType>(GST_MESSAGE_TOC|GST_MESSAGE_TAG);
|
||||
while (msg_filter != 0 && (msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND * 5, msg_filter))) {
|
||||
|
||||
// Handle TOC message: get tracks duration
|
||||
if (msg_toc) {
|
||||
GstToc *toc = nullptr;
|
||||
gst_message_parse_toc(msg_toc, &toc, nullptr);
|
||||
if (toc) {
|
||||
const QScopeGuard scopeguard_msg = qScopeGuard([msg]() {
|
||||
gst_message_unref(msg);
|
||||
});
|
||||
|
||||
if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TOC) {
|
||||
GstToc *toc = nullptr;
|
||||
gst_message_parse_toc(msg, &toc, nullptr);
|
||||
const QScopeGuard scopeguard_toc = qScopeGuard([toc]() {
|
||||
gst_toc_unref(toc);
|
||||
});
|
||||
GList *entries = gst_toc_get_entries(toc);
|
||||
if (entries && static_cast<guint>(songs.size()) <= g_list_length(entries)) {
|
||||
int i = 0;
|
||||
for (GList *node = entries; node != nullptr; node = node->next) {
|
||||
GstTocEntry *entry = static_cast<GstTocEntry*>(node->data);
|
||||
qint64 duration = 0;
|
||||
int track_number = 0;
|
||||
for (GList *entry_node = entries; entry_node != nullptr; entry_node = entry_node->next) {
|
||||
++track_number;
|
||||
if (songs.contains(track_number)) {
|
||||
Song &song = songs[track_number];
|
||||
GstTocEntry *entry = static_cast<GstTocEntry*>(entry_node->data);
|
||||
gint64 start = 0, stop = 0;
|
||||
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) duration = stop - start;
|
||||
songs[i++].set_length_nanosec(duration);
|
||||
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) {
|
||||
song.set_length_nanosec(static_cast<qint64>(stop - start));
|
||||
}
|
||||
}
|
||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^ GST_MESSAGE_TOC);
|
||||
}
|
||||
}
|
||||
gst_message_unref(msg_toc);
|
||||
}
|
||||
Q_EMIT SongsDurationLoaded(songs);
|
||||
|
||||
else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
|
||||
|
||||
GstTagList *tags = nullptr;
|
||||
gst_message_parse_tag(msg, &tags);
|
||||
const QScopeGuard scopeguard_tags = qScopeGuard([tags]() {
|
||||
gst_tag_list_free(tags);
|
||||
});
|
||||
|
||||
gint64 track_index = 0;
|
||||
gst_element_query_position(cdda, format_track, &track_index);
|
||||
|
||||
char *tag = nullptr;
|
||||
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
// Handle TAG message: generate MusicBrainz DiscId
|
||||
if (msg_tag) {
|
||||
GstTagList *tags = nullptr;
|
||||
gst_message_parse_tag(msg_tag, &tags);
|
||||
char *string_mb = nullptr;
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID, &string_mb)) {
|
||||
QString musicbrainz_discid = QString::fromUtf8(string_mb);
|
||||
qLog(Info) << "MusicBrainz discid: " << musicbrainz_discid;
|
||||
if (musicbrainz_discid.isEmpty()) {
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID, &tag)) {
|
||||
musicbrainz_discid = QString::fromUtf8(tag);
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
guint track_number = 0;
|
||||
if (!gst_tag_list_get_uint(tags, GST_TAG_TRACK_NUMBER, &track_number)) {
|
||||
qLog(Error) << "Could not get track number";
|
||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!songs.contains(track_number)) {
|
||||
qLog(Error) << "Got invalid track number" << track_number;
|
||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
|
||||
continue;
|
||||
}
|
||||
|
||||
Song &song = songs[track_number];
|
||||
guint64 duration = 0;
|
||||
if (gst_tag_list_get_uint64(tags, GST_TAG_DURATION, &duration)) {
|
||||
song.set_length_nanosec(static_cast<qint64>(duration));
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_ARTIST, &tag)) {
|
||||
song.set_albumartist(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_ARTIST_SORTNAME, &tag)) {
|
||||
song.set_albumartistsort(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ARTIST, &tag)) {
|
||||
song.set_artist(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
++track_artist_tags;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ARTIST_SORTNAME, &tag)) {
|
||||
song.set_artistsort(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM, &tag)) {
|
||||
song.set_album(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
++track_album_tags;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_SORTNAME, &tag)) {
|
||||
song.set_albumsort(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_TITLE, &tag)) {
|
||||
song.set_title(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
++track_title_tags;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_TITLE_SORTNAME, &tag)) {
|
||||
song.set_titlesort(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_GENRE, &tag)) {
|
||||
song.set_genre(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_COMPOSER, &tag)) {
|
||||
song.set_composer(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_COMPOSER_SORTNAME, &tag)) {
|
||||
song.set_composersort(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_PERFORMER, &tag)) {
|
||||
song.set_performer(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_COMMENT, &tag)) {
|
||||
song.set_comment(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
guint bitrate = 0;
|
||||
if (gst_tag_list_get_uint(tags, GST_TAG_BITRATE, &bitrate)) {
|
||||
song.set_bitrate(static_cast<int>(bitrate));
|
||||
}
|
||||
|
||||
if (track_number >= total_tracks) {
|
||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
|
||||
continue;
|
||||
}
|
||||
|
||||
const gint64 next_track_index = track_index + 1;
|
||||
if (!gst_element_seek_simple(pipeline, format_track, static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_TRICKMODE), next_track_index)) {
|
||||
qLog(Error) << "Failed to seek to next track index" << next_track_index;
|
||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
|
||||
}
|
||||
|
||||
MusicBrainzClient *musicbrainz_client = new MusicBrainzClient(network_);
|
||||
QObject::connect(musicbrainz_client, &MusicBrainzClient::DiscIdFinished, this, &CddaSongLoader::AudioCDTagsLoaded);
|
||||
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
|
||||
g_free(string_mb);
|
||||
gst_message_unref(msg_tag);
|
||||
gst_tag_list_unref(tags);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||||
// This will also cause cdda_ to be unref'd.
|
||||
// This will also cause cdda to be unref'd.
|
||||
gst_object_unref(pipeline);
|
||||
|
||||
if ((track_artist_tags >= total_tracks && track_album_tags >= total_tracks && track_title_tags >= total_tracks)) {
|
||||
qLog(Info) << "Songs loaded from CD-Text";
|
||||
Q_EMIT SongsUpdated(songs.values());
|
||||
Q_EMIT LoadingFinished();
|
||||
}
|
||||
else {
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
if (musicbrainz_discid.isEmpty()) {
|
||||
qLog(Info) << "CD is missing tags";
|
||||
Q_EMIT LoadingFinished();
|
||||
}
|
||||
else {
|
||||
qLog(Info) << "MusicBrainz Disc ID:" << musicbrainz_discid;
|
||||
Q_EMIT LoadTagsFromMusicBrainz(musicbrainz_discid, songs);
|
||||
}
|
||||
#else
|
||||
Q_EMIT LoadingFinished();
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
void CddaSongLoader::AudioCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results) {
|
||||
|
||||
MusicBrainzClient *musicbrainz_client = qobject_cast<MusicBrainzClient*>(sender());
|
||||
musicbrainz_client->deleteLater();
|
||||
if (results.empty()) return;
|
||||
SongList songs;
|
||||
songs.reserve(results.count());
|
||||
int track_number = 1;
|
||||
for (const MusicBrainzClient::Result &ret : results) {
|
||||
Song song(Song::Source::CDDA);
|
||||
song.set_artist(artist);
|
||||
song.set_album(album);
|
||||
song.set_title(ret.title_);
|
||||
song.set_length_nanosec(ret.duration_msec_ * kNsecPerMsec);
|
||||
song.set_track(track_number);
|
||||
song.set_year(ret.year_);
|
||||
song.set_id(track_number);
|
||||
song.set_filetype(Song::FileType::CDDA);
|
||||
song.set_valid(true);
|
||||
// We need to set url: that's how playlist will find the correct item to update
|
||||
song.set_url(GetUrlFromTrack(track_number++));
|
||||
songs << song;
|
||||
}
|
||||
Q_EMIT SongsMetadataLoaded(songs);
|
||||
void CDDASongLoader::LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid, const QMap<int, Song> &songs) {
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
bool CddaSongLoader::HasChanged() {
|
||||
|
||||
if (cdio_ && cdio_get_media_changed(cdio_) != 1) {
|
||||
return false;
|
||||
}
|
||||
// Check if mutex is already token (i.e. init is already taking place)
|
||||
if (!mutex_load_.tryLock()) {
|
||||
return false;
|
||||
}
|
||||
mutex_load_.unlock();
|
||||
|
||||
return true;
|
||||
musicbrainz_discid_ = musicbrainz_discid;
|
||||
musicbrainz_songs_ = songs;
|
||||
musicbrainz_client_->StartDiscIdRequest(musicbrainz_discid);
|
||||
|
||||
}
|
||||
|
||||
void CddaSongLoader::Error(const QString &error) {
|
||||
void CDDASongLoader::LoadTagsFromMusicBrainzFinished(const QString &musicbrainz_discid, const MusicBrainzClient::ResultList &results, const QString &error) {
|
||||
|
||||
if (musicbrainz_discid != musicbrainz_discid_) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMap<int, Song> songs = musicbrainz_songs_;
|
||||
musicbrainz_discid_.clear();
|
||||
musicbrainz_songs_.clear();
|
||||
|
||||
if (!error.isEmpty()) {
|
||||
Error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.empty()) {
|
||||
Q_EMIT LoadingFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const MusicBrainzClient::Result &result : results) {
|
||||
if (songs.contains(result.track_)) {
|
||||
Song &song = songs[result.track_];
|
||||
song.set_valid(true);
|
||||
song.set_id(result.track_);
|
||||
song.set_track(result.track_);
|
||||
song.set_artist(result.artist_);
|
||||
song.set_artistsort(result.sort_artist_);
|
||||
song.set_album(result.album_);
|
||||
song.set_title(result.title_);
|
||||
song.set_track(result.track_);
|
||||
song.set_year(result.year_);
|
||||
song.set_url(GetUrlFromTrack(song.track()));
|
||||
if (song.length_nanosec() <= 0) {
|
||||
song.set_length_nanosec(result.duration_msec_ * kNsecPerMsec);
|
||||
}
|
||||
if (!result.album_artist_.isEmpty() && result.album_artist_ != result.artist_) {
|
||||
song.set_albumartist(result.album_artist_);
|
||||
}
|
||||
if (!result.sort_album_artist_.isEmpty() && result.sort_album_artist_ != result.sort_artist_) {
|
||||
song.set_albumartistsort(result.sort_album_artist_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Q_EMIT SongsUpdated(songs.values());
|
||||
Q_EMIT LoadingFinished();
|
||||
|
||||
}
|
||||
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
|
||||
void CDDASongLoader::Error(const QString &error) {
|
||||
|
||||
qLog(Error) << error;
|
||||
Q_EMIT SongsDurationLoaded(SongList(), error);
|
||||
|
||||
Q_EMIT LoadError(error);
|
||||
Q_EMIT LoadingFinished();
|
||||
|
||||
}
|
||||
|
||||
@@ -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,16 +24,13 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include <cdio/types.h>
|
||||
#include <cdio/cdio.h>
|
||||
|
||||
#include <gst/gstelement.h>
|
||||
#include <gst/audio/gstaudiocdsrc.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
#include <QFuture>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
@@ -45,39 +42,48 @@
|
||||
|
||||
class NetworkAccessManager;
|
||||
|
||||
// This class provides a (hopefully) nice, high level interface to get CD information and load tracks
|
||||
class CddaSongLoader : public QObject {
|
||||
class CDDASongLoader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CddaSongLoader(const QUrl &url, QObject *parent = nullptr);
|
||||
~CddaSongLoader() override;
|
||||
explicit CDDASongLoader(const QUrl &url, QObject *parent = nullptr);
|
||||
~CDDASongLoader() override;
|
||||
|
||||
// Load songs. Signals declared below will be emitted anytime new information will be available.
|
||||
void LoadSongs();
|
||||
bool HasChanged();
|
||||
|
||||
bool IsActive() const { return loading_future_.isRunning(); }
|
||||
|
||||
private:
|
||||
void LoadSongsFromCDDA();
|
||||
void Error(const QString &error);
|
||||
QUrl GetUrlFromTrack(const int track_number) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void SongsLoadError(const QString &error);
|
||||
void SongsLoaded(const SongList &songs);
|
||||
void SongsDurationLoaded(const SongList &songs, const QString &error = QString());
|
||||
void SongsMetadataLoaded(const SongList &songs);
|
||||
void SongsUpdated(const SongList &songs);
|
||||
void LoadError(const QString &error);
|
||||
void LoadingFinished();
|
||||
void LoadTagsFromMusicBrainz(const QString &musicbrainz_discid, const QMap<int, Song> &songs);
|
||||
|
||||
private Q_SLOTS:
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
void AudioCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results);
|
||||
void LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid, const QMap<int, Song> &songs);
|
||||
void LoadTagsFromMusicBrainzFinished(const QString &musicbrainz_discid, const MusicBrainzClient::ResultList &results, const QString &error);
|
||||
#endif
|
||||
|
||||
private:
|
||||
const QUrl url_;
|
||||
SharedPtr<NetworkAccessManager> network_;
|
||||
GstElement *cdda_;
|
||||
CdIo_t *cdio_;
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
MusicBrainzClient *musicbrainz_client_;
|
||||
#endif
|
||||
QMutex mutex_load_;
|
||||
QFuture<void> loading_future_;
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
QString musicbrainz_discid_;
|
||||
QMap<int, Song> musicbrainz_songs_;
|
||||
#endif
|
||||
bool whatever_;
|
||||
};
|
||||
|
||||
#endif // CDDASONGLOADER_H
|
||||
|
||||
@@ -67,9 +67,6 @@ class ConnectedDevice : public QObject, public virtual MusicStorage, public enab
|
||||
virtual bool IsLoading() { return false; }
|
||||
virtual void NewConnection() {}
|
||||
virtual void ConnectAsync();
|
||||
// For some devices (e.g. CD devices) we don't have callbacks to be notified when something change:
|
||||
// we can call this method to refresh device's state
|
||||
virtual void Refresh() {}
|
||||
|
||||
TranscodeMode GetTranscodeMode() const override;
|
||||
Song::FileType GetTranscodeFormat() const override;
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
constexpr int kDeviceSchemaVersion = 5;
|
||||
constexpr int kDeviceSchemaVersion = 6;
|
||||
}
|
||||
|
||||
DeviceDatabaseBackend::DeviceDatabaseBackend(QObject *parent)
|
||||
|
||||
@@ -36,61 +36,75 @@
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
DeviceDatabaseBackend::Device DeviceInfo::SaveToDb() const {
|
||||
void DeviceInfo::InitFromDb(const DeviceDatabaseBackend::Device &device) {
|
||||
|
||||
DeviceDatabaseBackend::Device ret;
|
||||
ret.friendly_name_ = friendly_name_;
|
||||
ret.size_ = size_;
|
||||
ret.id_ = database_id_;
|
||||
ret.icon_name_ = icon_name_;
|
||||
ret.transcode_mode_ = transcode_mode_;
|
||||
ret.transcode_format_ = transcode_format_;
|
||||
database_id_ = device.id_;
|
||||
friendly_name_ = device.friendly_name_;
|
||||
size_ = device.size_;
|
||||
transcode_mode_ = device.transcode_mode_;
|
||||
transcode_format_ = device.transcode_format_;
|
||||
icon_name_ = device.icon_name_;
|
||||
|
||||
QStringList unique_ids;
|
||||
unique_ids.reserve(backends_.count());
|
||||
for (const Backend &backend : backends_) {
|
||||
unique_ids << backend.unique_id_;
|
||||
}
|
||||
ret.unique_id_ = unique_ids.join(u',');
|
||||
InitIcon();
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void DeviceInfo::InitFromDb(const DeviceDatabaseBackend::Device &dev) {
|
||||
|
||||
database_id_ = dev.id_;
|
||||
friendly_name_ = dev.friendly_name_;
|
||||
size_ = dev.size_;
|
||||
transcode_mode_ = dev.transcode_mode_;
|
||||
transcode_format_ = dev.transcode_format_;
|
||||
icon_name_ = dev.icon_name_;
|
||||
|
||||
const QStringList unique_ids = dev.unique_id_.split(u',');
|
||||
const QStringList unique_ids = device.unique_id_.split(u',');
|
||||
for (const QString &id : unique_ids) {
|
||||
backends_ << Backend(nullptr, id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DeviceDatabaseBackend::Device DeviceInfo::SaveToDb() const {
|
||||
|
||||
DeviceDatabaseBackend::Device device;
|
||||
device.friendly_name_ = friendly_name_;
|
||||
device.size_ = size_;
|
||||
device.id_ = database_id_;
|
||||
device.icon_name_ = icon_name_;
|
||||
device.transcode_mode_ = transcode_mode_;
|
||||
device.transcode_format_ = transcode_format_;
|
||||
|
||||
QStringList unique_ids;
|
||||
unique_ids.reserve(backends_.count());
|
||||
for (const Backend &backend : backends_) {
|
||||
unique_ids << backend.unique_id_;
|
||||
}
|
||||
device.unique_id_ = unique_ids.join(u',');
|
||||
|
||||
return device;
|
||||
|
||||
}
|
||||
|
||||
const DeviceInfo::Backend *DeviceInfo::BestBackend() const {
|
||||
|
||||
int best_priority = -1;
|
||||
const Backend *ret = nullptr;
|
||||
const Backend *backend = nullptr;
|
||||
|
||||
for (int i = 0; i < backends_.count(); ++i) {
|
||||
if (backends_[i].lister_ && backends_[i].lister_->priority() > best_priority) {
|
||||
best_priority = backends_[i].lister_->priority();
|
||||
ret = &(backends_[i]);
|
||||
backend = &(backends_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ret && !backends_.isEmpty()) return &(backends_[0]);
|
||||
return ret;
|
||||
if (!backend && !backends_.isEmpty()) return &(backends_[0]);
|
||||
return backend;
|
||||
|
||||
}
|
||||
|
||||
void DeviceInfo::SetIcon(const QVariantList &icons, const QString &name_hint) {
|
||||
void DeviceInfo::InitIcon() {
|
||||
|
||||
const QStringList icon_name_list = icon_name_.split(u',');
|
||||
QVariantList icons;
|
||||
icons.reserve(icon_name_list.count());
|
||||
for (const QString &icon_name : icon_name_list) {
|
||||
icons << icon_name;
|
||||
}
|
||||
LoadIcon(icons, friendly_name_);
|
||||
|
||||
}
|
||||
|
||||
void DeviceInfo::LoadIcon(const QVariantList &icons, const QString &name_hint) {
|
||||
|
||||
icon_name_ = "device"_L1;
|
||||
|
||||
|
||||
@@ -97,8 +97,9 @@ class DeviceInfo : public SimpleTreeItem<DeviceInfo> {
|
||||
void InitFromDb(const DeviceDatabaseBackend::Device &dev);
|
||||
DeviceDatabaseBackend::Device SaveToDb() const;
|
||||
|
||||
void InitIcon();
|
||||
// Tries to load a good icon for the device. Sets icon_name_ and icon_.
|
||||
void SetIcon(const QVariantList &icons, const QString &name_hint);
|
||||
void LoadIcon(const QVariantList &icons, const QString &name_hint);
|
||||
|
||||
// Gets the best backend available (the one with the highest priority)
|
||||
const Backend *BestBackend() const;
|
||||
|
||||
@@ -109,7 +109,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
|
||||
backend_->moveToThread(database->thread());
|
||||
backend_->Init(database);
|
||||
|
||||
QObject::connect(this, &DeviceManager::DeviceCreatedFromDB, this, &DeviceManager::AddDeviceFromDB);
|
||||
QObject::connect(this, &DeviceManager::DevicesLoaded, this, &DeviceManager::AddDevicesFromDB);
|
||||
|
||||
// This reads from the database and contents on the database mutex, which can be very slow on startup.
|
||||
(void)QtConcurrent::run(&thread_pool_, &DeviceManager::LoadAllDevices, this);
|
||||
@@ -120,7 +120,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
|
||||
|
||||
// CD devices are detected via the DiskArbitration framework instead on MacOs.
|
||||
#if defined(HAVE_AUDIOCD) && !defined(Q_OS_MACOS)
|
||||
AddLister(new CddaLister);
|
||||
AddLister(new CDDALister);
|
||||
#endif
|
||||
#ifdef HAVE_UDISKS2
|
||||
AddLister(new Udisks2Lister);
|
||||
@@ -133,7 +133,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
AddDeviceClass<CddaDevice>();
|
||||
AddDeviceClass<CDDADevice>();
|
||||
#endif
|
||||
|
||||
AddDeviceClass<FilesystemDevice>();
|
||||
@@ -167,12 +167,12 @@ void DeviceManager::Exit() {
|
||||
|
||||
void DeviceManager::CloseDevices() {
|
||||
|
||||
for (DeviceInfo *info : std::as_const(devices_)) {
|
||||
if (!info->device_) continue;
|
||||
if (wait_for_exit_.contains(&*info->device_)) continue;
|
||||
wait_for_exit_ << &*info->device_;
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::destroyed, this, &DeviceManager::DeviceDestroyed);
|
||||
info->device_->Close();
|
||||
for (DeviceInfo *device_info : std::as_const(devices_)) {
|
||||
if (!device_info->device_) continue;
|
||||
if (wait_for_exit_.contains(&*device_info->device_)) continue;
|
||||
wait_for_exit_ << &*device_info->device_;
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::destroyed, this, &DeviceManager::DeviceDestroyed);
|
||||
device_info->device_->Close();
|
||||
}
|
||||
if (wait_for_exit_.isEmpty()) CloseListers();
|
||||
|
||||
@@ -224,10 +224,10 @@ void DeviceManager::ListerClosed() {
|
||||
|
||||
void DeviceManager::DeviceDestroyed() {
|
||||
|
||||
ConnectedDevice *device = static_cast<ConnectedDevice*>(sender());
|
||||
if (!wait_for_exit_.contains(device) || !backend_) return;
|
||||
ConnectedDevice *connected_device = static_cast<ConnectedDevice*>(sender());
|
||||
if (!wait_for_exit_.contains(connected_device) || !backend_) return;
|
||||
|
||||
wait_for_exit_.removeAll(device);
|
||||
wait_for_exit_.removeAll(connected_device);
|
||||
if (wait_for_exit_.isEmpty()) CloseListers();
|
||||
|
||||
}
|
||||
@@ -237,41 +237,37 @@ void DeviceManager::LoadAllDevices() {
|
||||
Q_ASSERT(QThread::currentThread() != qApp->thread());
|
||||
|
||||
const DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices();
|
||||
for (const DeviceDatabaseBackend::Device &device : devices) {
|
||||
DeviceInfo *info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||
info->InitFromDb(device);
|
||||
Q_EMIT DeviceCreatedFromDB(info);
|
||||
}
|
||||
|
||||
Q_EMIT DevicesLoaded(devices);
|
||||
|
||||
// This is done in a concurrent thread so close the unique DB connection.
|
||||
backend_->Close();
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::AddDeviceFromDB(DeviceInfo *info) {
|
||||
void DeviceManager::AddDevicesFromDB(const DeviceDatabaseBackend::DeviceList &devices) {
|
||||
|
||||
const QStringList icon_names = info->icon_name_.split(u',');
|
||||
QVariantList icons;
|
||||
icons.reserve(icon_names.count());
|
||||
for (const QString &icon_name : icon_names) {
|
||||
icons << icon_name;
|
||||
}
|
||||
info->SetIcon(icons, info->friendly_name_);
|
||||
|
||||
DeviceInfo *existing = FindEquivalentDevice(info);
|
||||
if (existing) {
|
||||
qLog(Info) << "Found existing device: " << info->friendly_name_;
|
||||
existing->icon_name_ = info->icon_name_;
|
||||
existing->icon_ = info->icon_;
|
||||
QModelIndex idx = ItemToIndex(existing);
|
||||
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
|
||||
root_->Delete(info->row);
|
||||
}
|
||||
else {
|
||||
qLog(Info) << "Device added from database: " << info->friendly_name_;
|
||||
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
|
||||
devices_ << info;
|
||||
endInsertRows();
|
||||
for (const DeviceDatabaseBackend::Device &device : devices) {
|
||||
const QStringList unique_ids = device.unique_id_.split(u',');
|
||||
DeviceInfo *device_info = FindEquivalentDevice(unique_ids);
|
||||
if (device_info && device_info->database_id_ == -1) {
|
||||
qLog(Info) << "Database device linked to physical device:" << device.friendly_name_;
|
||||
device_info->database_id_ = device.id_;
|
||||
device_info->icon_name_ = device.icon_name_;
|
||||
device_info->InitIcon();
|
||||
const QModelIndex idx = ItemToIndex(device_info);
|
||||
if (idx.isValid()) {
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
}
|
||||
else {
|
||||
qLog(Info) << "Database device:" << device.friendly_name_;
|
||||
device_info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||
device_info->InitFromDb(device);
|
||||
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
|
||||
devices_ << device_info;
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -280,30 +276,29 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
|
||||
|
||||
if (!idx.isValid() || idx.column() != 0) return QVariant();
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return QVariant();
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return QVariant();
|
||||
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:{
|
||||
QString text;
|
||||
if (!info->friendly_name_.isEmpty()) {
|
||||
text = info->friendly_name_;
|
||||
if (!device_info->friendly_name_.isEmpty()) {
|
||||
text = device_info->friendly_name_;
|
||||
}
|
||||
else if (info->BestBackend()) {
|
||||
text = info->BestBackend()->unique_id_;
|
||||
else if (device_info->BestBackend()) {
|
||||
text = device_info->BestBackend()->unique_id_;
|
||||
}
|
||||
|
||||
if (info->size_ > 0) {
|
||||
text = text + QStringLiteral(" (%1)").arg(Utilities::PrettySize(info->size_));
|
||||
if (device_info->size_ > 0) {
|
||||
text = text + QStringLiteral(" (%1)").arg(Utilities::PrettySize(device_info->size_));
|
||||
}
|
||||
if (info->device_) info->device_->Refresh();
|
||||
return text;
|
||||
}
|
||||
|
||||
case Qt::DecorationRole:{
|
||||
QPixmap pixmap = info->icon_.pixmap(kDeviceIconSize);
|
||||
QPixmap pixmap = device_info->icon_.pixmap(kDeviceIconSize);
|
||||
|
||||
if (info->backends_.isEmpty() || !info->BestBackend() || !info->BestBackend()->lister_) {
|
||||
if (device_info->backends_.isEmpty() || !device_info->BestBackend() || !device_info->BestBackend()->lister_) {
|
||||
// Disconnected but remembered
|
||||
QPainter p(&pixmap);
|
||||
p.drawPixmap(kDeviceIconSize - kDeviceIconOverlaySize, kDeviceIconSize - kDeviceIconOverlaySize, not_connected_overlay_.pixmap(kDeviceIconOverlaySize));
|
||||
@@ -313,62 +308,62 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
|
||||
}
|
||||
|
||||
case Role_FriendlyName:
|
||||
return info->friendly_name_;
|
||||
return device_info->friendly_name_;
|
||||
|
||||
case Role_UniqueId:
|
||||
if (!info->BestBackend()) return QString();
|
||||
return info->BestBackend()->unique_id_;
|
||||
if (!device_info->BestBackend()) return QString();
|
||||
return device_info->BestBackend()->unique_id_;
|
||||
|
||||
case Role_IconName:
|
||||
return info->icon_name_;
|
||||
return device_info->icon_name_;
|
||||
|
||||
case Role_Capacity:
|
||||
case MusicStorage::Role_Capacity:
|
||||
return info->size_;
|
||||
return device_info->size_;
|
||||
|
||||
case Role_FreeSpace:
|
||||
case MusicStorage::Role_FreeSpace:
|
||||
return ((info->BestBackend() && info->BestBackend()->lister_) ? info->BestBackend()->lister_->DeviceFreeSpace(info->BestBackend()->unique_id_) : QVariant());
|
||||
return ((device_info->BestBackend() && device_info->BestBackend()->lister_) ? device_info->BestBackend()->lister_->DeviceFreeSpace(device_info->BestBackend()->unique_id_) : QVariant());
|
||||
|
||||
case Role_State:
|
||||
if (info->device_) return State_Connected;
|
||||
if (info->BestBackend() && info->BestBackend()->lister_) {
|
||||
if (info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) return State_NotMounted;
|
||||
return State_NotConnected;
|
||||
if (device_info->device_) return QVariant::fromValue(State::Connected);
|
||||
if (device_info->BestBackend() && device_info->BestBackend()->lister_) {
|
||||
if (device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) return QVariant::fromValue(State::NotMounted);
|
||||
return QVariant::fromValue(State::NotConnected);
|
||||
}
|
||||
return State_Remembered;
|
||||
return QVariant::fromValue(State::Remembered);
|
||||
|
||||
case Role_UpdatingPercentage:
|
||||
if (info->task_percentage_ == -1) return QVariant();
|
||||
return info->task_percentage_;
|
||||
if (device_info->task_percentage_ == -1) return QVariant();
|
||||
return device_info->task_percentage_;
|
||||
|
||||
case MusicStorage::Role_Storage:
|
||||
if (!info->device_ && info->database_id_ != -1) {
|
||||
const_cast<DeviceManager*>(this)->Connect(info);
|
||||
if (!device_info->device_ && device_info->database_id_ != -1) {
|
||||
const_cast<DeviceManager*>(this)->Connect(device_info);
|
||||
}
|
||||
if (!info->device_) return QVariant();
|
||||
return QVariant::fromValue<SharedPtr<MusicStorage>>(info->device_);
|
||||
if (!device_info->device_) return QVariant();
|
||||
return QVariant::fromValue<SharedPtr<MusicStorage>>(device_info->device_);
|
||||
|
||||
case MusicStorage::Role_StorageForceConnect:
|
||||
if (!info->BestBackend()) return QVariant();
|
||||
if (!info->device_) {
|
||||
if (info->database_id_ == -1 && !info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) {
|
||||
if (info->BestBackend()->lister_->AskForScan(info->BestBackend()->unique_id_)) {
|
||||
if (!device_info->BestBackend()) return QVariant();
|
||||
if (!device_info->device_) {
|
||||
if (device_info->database_id_ == -1 && !device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) {
|
||||
if (device_info->BestBackend()->lister_->AskForScan(device_info->BestBackend()->unique_id_)) {
|
||||
ScopedPtr<QMessageBox> dialog(new QMessageBox(QMessageBox::Information, tr("Connect device"), tr("This is the first time you have connected this device. Strawberry will now scan the device to find music files - this may take some time."), QMessageBox::Cancel));
|
||||
QPushButton *pushbutton = dialog->addButton(tr("Connect device"), QMessageBox::AcceptRole);
|
||||
dialog->exec();
|
||||
if (dialog->clickedButton() != pushbutton) return QVariant();
|
||||
}
|
||||
}
|
||||
const_cast<DeviceManager*>(this)->Connect(info);
|
||||
const_cast<DeviceManager*>(this)->Connect(device_info);
|
||||
}
|
||||
if (!info->device_) return QVariant();
|
||||
return QVariant::fromValue<SharedPtr<MusicStorage>>(info->device_);
|
||||
if (!device_info->device_) return QVariant();
|
||||
return QVariant::fromValue<SharedPtr<MusicStorage>>(device_info->device_);
|
||||
|
||||
case Role_MountPath:{
|
||||
if (!info->device_) return QVariant();
|
||||
if (!device_info->device_) return QVariant();
|
||||
|
||||
QString ret = info->device_->url().path();
|
||||
QString ret = device_info->device_->url().path();
|
||||
#ifdef Q_OS_WIN32
|
||||
if (ret.startsWith(u'/')) ret.remove(0, 1);
|
||||
#endif
|
||||
@@ -376,17 +371,17 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
|
||||
}
|
||||
|
||||
case Role_TranscodeMode:
|
||||
return static_cast<int>(info->transcode_mode_);
|
||||
return static_cast<int>(device_info->transcode_mode_);
|
||||
|
||||
case Role_TranscodeFormat:
|
||||
return static_cast<int>(info->transcode_format_);
|
||||
return static_cast<int>(device_info->transcode_format_);
|
||||
|
||||
case Role_SongCount:
|
||||
if (!info->device_) return QVariant();
|
||||
return info->device_->song_count();
|
||||
if (!device_info->device_) return QVariant();
|
||||
return device_info->device_->song_count();
|
||||
|
||||
case Role_CopyMusic:
|
||||
if (info->BestBackend() && info->BestBackend()->lister_) return info->BestBackend()->lister_->CopyMusic();
|
||||
if (device_info->BestBackend() && device_info->BestBackend()->lister_) return device_info->BestBackend()->lister_->CopyMusic();
|
||||
else return false;
|
||||
|
||||
default:
|
||||
@@ -410,7 +405,9 @@ DeviceInfo *DeviceManager::FindDeviceById(const QString &id) const {
|
||||
|
||||
for (int i = 0; i < devices_.count(); ++i) {
|
||||
for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) {
|
||||
if (backend.unique_id_ == id) return devices_[i];
|
||||
if (backend.unique_id_ == id) {
|
||||
return devices_[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,10 +422,11 @@ DeviceInfo *DeviceManager::FindDeviceByUrl(const QList<QUrl> &urls) const {
|
||||
for (int i = 0; i < devices_.count(); ++i) {
|
||||
for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) {
|
||||
if (!backend.lister_) continue;
|
||||
|
||||
const QList<QUrl> device_urls = backend.lister_->MakeDeviceUrls(backend.unique_id_);
|
||||
for (const QUrl &url : device_urls) {
|
||||
if (urls.contains(url)) return devices_[i];
|
||||
if (urls.contains(url)) {
|
||||
return devices_[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,12 +435,15 @@ DeviceInfo *DeviceManager::FindDeviceByUrl(const QList<QUrl> &urls) const {
|
||||
|
||||
}
|
||||
|
||||
DeviceInfo *DeviceManager::FindEquivalentDevice(DeviceInfo *info) const {
|
||||
DeviceInfo *DeviceManager::FindEquivalentDevice(const QStringList &unique_ids) const {
|
||||
|
||||
for (const DeviceInfo::Backend &backend : std::as_const(info->backends_)) {
|
||||
DeviceInfo *match = FindDeviceById(backend.unique_id_);
|
||||
if (match) return match;
|
||||
for (const QString &unique_id : unique_ids) {
|
||||
DeviceInfo *device_info_match = FindDeviceById(unique_id);
|
||||
if (device_info_match) {
|
||||
return device_info_match;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
@@ -455,42 +456,42 @@ void DeviceManager::PhysicalDeviceAdded(const QString &id) {
|
||||
qLog(Info) << "Device added:" << id << lister->DeviceUniqueIDs();
|
||||
|
||||
// Do we have this device already?
|
||||
DeviceInfo *info = FindDeviceById(id);
|
||||
if (info) {
|
||||
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) {
|
||||
if (info->backends_[backend_index].unique_id_ == id) {
|
||||
info->backends_[backend_index].lister_ = lister;
|
||||
DeviceInfo *device_info = FindDeviceById(id);
|
||||
if (device_info) {
|
||||
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
|
||||
if (device_info->backends_[backend_index].unique_id_ == id) {
|
||||
device_info->backends_[backend_index].lister_ = lister;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
else {
|
||||
// Check if we have another device with the same URL
|
||||
info = FindDeviceByUrl(lister->MakeDeviceUrls(id));
|
||||
if (info) {
|
||||
device_info = FindDeviceByUrl(lister->MakeDeviceUrls(id));
|
||||
if (device_info) {
|
||||
// Add this device's lister to the existing device
|
||||
info->backends_ << DeviceInfo::Backend(lister, id);
|
||||
device_info->backends_ << DeviceInfo::Backend(lister, id);
|
||||
|
||||
// If the user hasn't saved the device in the DB yet then overwrite the device's name and icon etc.
|
||||
if (info->database_id_ == -1 && info->BestBackend() && info->BestBackend()->lister_ == lister) {
|
||||
info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||
info->size_ = lister->DeviceCapacity(id);
|
||||
info->SetIcon(lister->DeviceIcons(id), info->friendly_name_);
|
||||
if (device_info->database_id_ == -1 && device_info->BestBackend() && device_info->BestBackend()->lister_ == lister) {
|
||||
device_info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||
device_info->size_ = lister->DeviceCapacity(id);
|
||||
device_info->LoadIcon(lister->DeviceIcons(id), device_info->friendly_name_);
|
||||
}
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
else {
|
||||
// It's a completely new device
|
||||
info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||
info->backends_ << DeviceInfo::Backend(lister, id);
|
||||
info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||
info->size_ = lister->DeviceCapacity(id);
|
||||
info->SetIcon(lister->DeviceIcons(id), info->friendly_name_);
|
||||
device_info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||
device_info->backends_ << DeviceInfo::Backend(lister, id);
|
||||
device_info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||
device_info->size_ = lister->DeviceCapacity(id);
|
||||
device_info->LoadIcon(lister->DeviceIcons(id), device_info->friendly_name_);
|
||||
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
|
||||
devices_ << info;
|
||||
devices_ << device_info;
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
@@ -503,42 +504,42 @@ void DeviceManager::PhysicalDeviceRemoved(const QString &id) {
|
||||
|
||||
qLog(Info) << "Device removed:" << id;
|
||||
|
||||
DeviceInfo *info = FindDeviceById(id);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = FindDeviceById(id);
|
||||
if (!device_info) return;
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
const QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
if (info->database_id_ != -1) {
|
||||
if (device_info->database_id_ != -1) {
|
||||
// Keep the structure around, but just "disconnect" it
|
||||
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) {
|
||||
if (info->backends_[backend_index].unique_id_ == id) {
|
||||
info->backends_[backend_index].lister_ = nullptr;
|
||||
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
|
||||
if (device_info->backends_[backend_index].unique_id_ == id) {
|
||||
device_info->backends_[backend_index].lister_ = nullptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (info->device_ && info->device_->lister() == lister) {
|
||||
info->device_->Close();
|
||||
if (device_info->device_ && device_info->device_->lister() == lister) {
|
||||
device_info->device_->Close();
|
||||
}
|
||||
|
||||
if (!info->device_) Q_EMIT DeviceDisconnected(idx);
|
||||
if (!device_info->device_) Q_EMIT DeviceDisconnected(idx);
|
||||
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
else {
|
||||
// If this was the last lister for the device then remove it from the model
|
||||
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) {
|
||||
if (info->backends_[backend_index].unique_id_ == id) {
|
||||
info->backends_.removeAt(backend_index);
|
||||
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
|
||||
if (device_info->backends_[backend_index].unique_id_ == id) {
|
||||
device_info->backends_.removeAt(backend_index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (info->backends_.isEmpty()) {
|
||||
if (device_info->backends_.isEmpty()) {
|
||||
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
|
||||
devices_.removeAll(info);
|
||||
root_->Delete(info->row);
|
||||
devices_.removeAll(device_info);
|
||||
root_->Delete(device_info->row);
|
||||
endRemoveRows();
|
||||
}
|
||||
}
|
||||
@@ -550,8 +551,8 @@ void DeviceManager::PhysicalDeviceChanged(const QString &id) {
|
||||
DeviceLister *lister = qobject_cast<DeviceLister*>(sender());
|
||||
Q_UNUSED(lister);
|
||||
|
||||
DeviceInfo *info = FindDeviceById(id);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = FindDeviceById(id);
|
||||
if (!device_info) return;
|
||||
|
||||
// TODO
|
||||
|
||||
@@ -561,40 +562,41 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(const QModelIndex &idx) {
|
||||
|
||||
SharedPtr<ConnectedDevice> ret;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return ret;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return ret;
|
||||
|
||||
return Connect(info);
|
||||
return Connect(device_info);
|
||||
|
||||
}
|
||||
|
||||
SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
|
||||
SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *device_info) {
|
||||
|
||||
SharedPtr<ConnectedDevice> ret;
|
||||
|
||||
if (!info) return ret;
|
||||
if (info->device_) { // Already connected
|
||||
return info->device_;
|
||||
if (!device_info) {
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
|
||||
if (!info->BestBackend() || !info->BestBackend()->lister_) { // Not physically connected
|
||||
return ret;
|
||||
if (device_info->device_) { // Already connected
|
||||
return device_info->device_;
|
||||
}
|
||||
|
||||
if (info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) { // Mount the device
|
||||
info->BestBackend()->lister_->MountDeviceAsync(info->BestBackend()->unique_id_);
|
||||
return ret;
|
||||
if (!device_info->BestBackend() || !device_info->BestBackend()->lister_) { // Not physically connected
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
|
||||
bool first_time = (info->database_id_ == -1);
|
||||
if (device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) { // Mount the device
|
||||
device_info->BestBackend()->lister_->MountDeviceAsync(device_info->BestBackend()->unique_id_);
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
|
||||
const bool first_time = device_info->database_id_ == -1;
|
||||
if (first_time) {
|
||||
// We haven't stored this device in the database before
|
||||
info->database_id_ = backend_->AddDevice(info->SaveToDb());
|
||||
device_info->database_id_ = backend_->AddDevice(device_info->SaveToDb());
|
||||
}
|
||||
|
||||
// Get the device URLs
|
||||
const QList<QUrl> urls = info->BestBackend()->lister_->MakeDeviceUrls(info->BestBackend()->unique_id_);
|
||||
if (urls.isEmpty()) return ret;
|
||||
const QList<QUrl> urls = device_info->BestBackend()->lister_->MakeDeviceUrls(device_info->BestBackend()->unique_id_);
|
||||
if (urls.isEmpty()) return SharedPtr<ConnectedDevice>();
|
||||
|
||||
// Take the first URL that we have a handler for
|
||||
QUrl device_url;
|
||||
@@ -614,7 +616,7 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
|
||||
tr("This is an MTP device, but you compiled Strawberry without libmtp support.") + u" "_s +
|
||||
tr("If you continue, this device will work slowly and songs copied to it may not work."),
|
||||
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
|
||||
return ret;
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
|
||||
if (url.scheme() == "ipod"_L1) {
|
||||
@@ -622,7 +624,7 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
|
||||
tr("This is an iPod, but you compiled Strawberry without libgpod support.") + " "_L1 +
|
||||
tr("If you continue, this device will work slowly and songs copied to it may not work."),
|
||||
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
|
||||
return ret;
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,114 +637,114 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
|
||||
}
|
||||
|
||||
Q_EMIT DeviceError(tr("This type of device is not supported: %1").arg(url_strings.join(", "_L1)));
|
||||
return ret;
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
|
||||
QMetaObject meta_object = device_classes_.value(device_url.scheme());
|
||||
QObject *instance = meta_object.newInstance(
|
||||
Q_ARG(QUrl, device_url),
|
||||
Q_ARG(DeviceLister*, info->BestBackend()->lister_),
|
||||
Q_ARG(QString, info->BestBackend()->unique_id_),
|
||||
Q_ARG(DeviceLister*, device_info->BestBackend()->lister_),
|
||||
Q_ARG(QString, device_info->BestBackend()->unique_id_),
|
||||
Q_ARG(DeviceManager*, this),
|
||||
Q_ARG(SharedPtr<TaskManager>, task_manager_),
|
||||
Q_ARG(SharedPtr<Database>, database_),
|
||||
Q_ARG(SharedPtr<TagReaderClient>, tagreader_client_),
|
||||
Q_ARG(SharedPtr<AlbumCoverLoader>, albumcover_loader_),
|
||||
Q_ARG(int, info->database_id_),
|
||||
Q_ARG(int, device_info->database_id_),
|
||||
Q_ARG(bool, first_time));
|
||||
|
||||
ret.reset(qobject_cast<ConnectedDevice*>(instance));
|
||||
SharedPtr<ConnectedDevice> connected_device = SharedPtr<ConnectedDevice>(qobject_cast<ConnectedDevice*>(instance));
|
||||
|
||||
if (!ret) {
|
||||
if (!connected_device) {
|
||||
qLog(Warning) << "Could not create device for" << device_url.toString();
|
||||
return ret;
|
||||
return connected_device;
|
||||
}
|
||||
|
||||
bool result = ret->Init();
|
||||
bool result = connected_device->Init();
|
||||
if (!result) {
|
||||
qLog(Warning) << "Could not connect to device" << device_url.toString();
|
||||
return ret;
|
||||
return connected_device;
|
||||
}
|
||||
info->device_ = ret;
|
||||
device_info->device_ = connected_device;
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
if (!idx.isValid()) return ret;
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return connected_device;
|
||||
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::TaskStarted, this, &DeviceManager::DeviceTaskStarted);
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::SongCountUpdated, this, &DeviceManager::DeviceSongCountUpdated);
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::DeviceConnectFinished, this, &DeviceManager::DeviceConnectFinished);
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::DeviceCloseFinished, this, &DeviceManager::DeviceCloseFinished);
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::Error, this, &DeviceManager::DeviceError);
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::TaskStarted, this, &DeviceManager::DeviceTaskStarted);
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::SongCountUpdated, this, &DeviceManager::DeviceSongCountUpdated);
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::DeviceConnectFinished, this, &DeviceManager::DeviceConnectFinished);
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::DeviceCloseFinished, this, &DeviceManager::DeviceCloseFinished);
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::Error, this, &DeviceManager::DeviceError);
|
||||
|
||||
ret->ConnectAsync();
|
||||
connected_device->ConnectAsync();
|
||||
|
||||
return ret;
|
||||
return connected_device;
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::DeviceConnectFinished(const QString &id, const bool success) {
|
||||
|
||||
DeviceInfo *info = FindDeviceById(id);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = FindDeviceById(id);
|
||||
if (!device_info) return;
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
if (success) {
|
||||
Q_EMIT DeviceConnected(idx);
|
||||
}
|
||||
else {
|
||||
info->device_->Close();
|
||||
device_info->device_->Close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::DeviceCloseFinished(const QString &id) {
|
||||
|
||||
DeviceInfo *info = FindDeviceById(id);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = FindDeviceById(id);
|
||||
if (!device_info) return;
|
||||
|
||||
info->device_.reset();
|
||||
device_info->device_.reset();
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
Q_EMIT DeviceDisconnected(idx);
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
|
||||
if (info->unmount_ && info->BestBackend() && info->BestBackend()->lister_) {
|
||||
info->BestBackend()->lister_->UnmountDeviceAsync(info->BestBackend()->unique_id_);
|
||||
if (device_info->unmount_ && device_info->BestBackend() && device_info->BestBackend()->lister_) {
|
||||
device_info->BestBackend()->lister_->UnmountDeviceAsync(device_info->BestBackend()->unique_id_);
|
||||
}
|
||||
|
||||
if (info->forget_) {
|
||||
RemoveFromDB(info, idx);
|
||||
if (device_info->forget_) {
|
||||
RemoveFromDB(device_info, idx);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DeviceInfo *DeviceManager::GetDevice(const QModelIndex &idx) const {
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
return info;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
return device_info;
|
||||
|
||||
}
|
||||
|
||||
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(const QModelIndex &idx) const {
|
||||
|
||||
SharedPtr<ConnectedDevice> ret;
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return ret;
|
||||
return info->device_;
|
||||
SharedPtr<ConnectedDevice> connected_device;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return connected_device;
|
||||
return device_info->device_;
|
||||
|
||||
}
|
||||
|
||||
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(DeviceInfo *info) const {
|
||||
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(DeviceInfo *device_info) const {
|
||||
|
||||
SharedPtr<ConnectedDevice> ret;
|
||||
if (!info) return ret;
|
||||
return info->device_;
|
||||
SharedPtr<ConnectedDevice> connected_device;
|
||||
if (!device_info) return connected_device;
|
||||
return device_info->device_;
|
||||
|
||||
}
|
||||
|
||||
@@ -750,9 +752,9 @@ int DeviceManager::GetDatabaseId(const QModelIndex &idx) const {
|
||||
|
||||
if (!idx.isValid()) return -1;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return -1;
|
||||
return info->database_id_;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return -1;
|
||||
return device_info->database_id_;
|
||||
|
||||
}
|
||||
|
||||
@@ -760,17 +762,17 @@ DeviceLister *DeviceManager::GetLister(const QModelIndex &idx) const {
|
||||
|
||||
if (!idx.isValid()) return nullptr;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info || !info->BestBackend()) return nullptr;
|
||||
return info->BestBackend()->lister_;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info || !device_info->BestBackend()) return nullptr;
|
||||
return device_info->BestBackend()->lister_;
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::Disconnect(DeviceInfo *info, const QModelIndex &idx) {
|
||||
void DeviceManager::Disconnect(DeviceInfo *device_info, const QModelIndex &idx) {
|
||||
|
||||
Q_UNUSED(idx);
|
||||
|
||||
info->device_->Close();
|
||||
device_info->device_->Close();
|
||||
|
||||
}
|
||||
|
||||
@@ -778,37 +780,37 @@ void DeviceManager::Forget(const QModelIndex &idx) {
|
||||
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return;
|
||||
|
||||
if (info->database_id_ == -1) return;
|
||||
if (device_info->database_id_ == -1) return;
|
||||
|
||||
if (info->device_) {
|
||||
info->forget_ = true;
|
||||
Disconnect(info, idx);
|
||||
if (device_info->device_) {
|
||||
device_info->forget_ = true;
|
||||
Disconnect(device_info, idx);
|
||||
}
|
||||
else {
|
||||
RemoveFromDB(info, idx);
|
||||
RemoveFromDB(device_info, idx);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::RemoveFromDB(DeviceInfo *info, const QModelIndex &idx) {
|
||||
void DeviceManager::RemoveFromDB(DeviceInfo *device_info, const QModelIndex &idx) {
|
||||
|
||||
backend_->RemoveDevice(info->database_id_);
|
||||
info->database_id_ = -1;
|
||||
backend_->RemoveDevice(device_info->database_id_);
|
||||
device_info->database_id_ = -1;
|
||||
|
||||
if (!info->BestBackend() || !info->BestBackend()->lister_) { // It's not attached any more so remove it from the list
|
||||
if (!device_info->BestBackend() || !device_info->BestBackend()->lister_) { // It's not attached any more so remove it from the list
|
||||
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
|
||||
devices_.removeAll(info);
|
||||
root_->Delete(info->row);
|
||||
devices_.removeAll(device_info);
|
||||
root_->Delete(device_info->row);
|
||||
endRemoveRows();
|
||||
}
|
||||
else { // It's still attached, set the name and icon back to what they were originally
|
||||
const QString id = info->BestBackend()->unique_id_;
|
||||
const QString id = device_info->BestBackend()->unique_id_;
|
||||
|
||||
info->friendly_name_ = info->BestBackend()->lister_->MakeFriendlyName(id);
|
||||
info->SetIcon(info->BestBackend()->lister_->DeviceIcons(id), info->friendly_name_);
|
||||
device_info->friendly_name_ = device_info->BestBackend()->lister_->MakeFriendlyName(id);
|
||||
device_info->LoadIcon(device_info->BestBackend()->lister_->DeviceIcons(id), device_info->friendly_name_);
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
|
||||
@@ -818,18 +820,18 @@ void DeviceManager::SetDeviceOptions(const QModelIndex &idx, const QString &frie
|
||||
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return;
|
||||
|
||||
info->friendly_name_ = friendly_name;
|
||||
info->SetIcon(QVariantList() << icon_name, friendly_name);
|
||||
info->transcode_mode_ = mode;
|
||||
info->transcode_format_ = format;
|
||||
device_info->friendly_name_ = friendly_name;
|
||||
device_info->LoadIcon(QVariantList() << icon_name, friendly_name);
|
||||
device_info->transcode_mode_ = mode;
|
||||
device_info->transcode_format_ = format;
|
||||
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
|
||||
if (info->database_id_ != -1) {
|
||||
backend_->SetDeviceOptions(info->database_id_, friendly_name, icon_name, mode, format);
|
||||
if (device_info->database_id_ != -1) {
|
||||
backend_->SetDeviceOptions(device_info->database_id_, friendly_name, icon_name, mode, format);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -840,12 +842,12 @@ void DeviceManager::DeviceTaskStarted(const int id) {
|
||||
if (!device) return;
|
||||
|
||||
for (int i = 0; i < devices_.count(); ++i) {
|
||||
DeviceInfo *info = devices_.value(i);
|
||||
if (info->device_ && &*info->device_ == device) {
|
||||
QModelIndex index = ItemToIndex(info);
|
||||
DeviceInfo *device_info = devices_.value(i);
|
||||
if (device_info->device_ && &*device_info->device_ == device) {
|
||||
QModelIndex index = ItemToIndex(device_info);
|
||||
if (!index.isValid()) continue;
|
||||
active_tasks_[id] = index;
|
||||
info->task_percentage_ = 0;
|
||||
device_info->task_percentage_ = 0;
|
||||
Q_EMIT dataChanged(index, index);
|
||||
return;
|
||||
}
|
||||
@@ -864,12 +866,12 @@ void DeviceManager::TasksChanged() {
|
||||
const QPersistentModelIndex idx = active_tasks_.value(task.id);
|
||||
if (!idx.isValid()) continue;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (task.progress_max) {
|
||||
info->task_percentage_ = static_cast<int>(static_cast<float>(task.progress) / static_cast<float>(task.progress_max) * 100);
|
||||
device_info->task_percentage_ = static_cast<int>(static_cast<float>(task.progress) / static_cast<float>(task.progress_max) * 100);
|
||||
}
|
||||
else {
|
||||
info->task_percentage_ = 0;
|
||||
device_info->task_percentage_ = 0;
|
||||
}
|
||||
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
@@ -881,10 +883,10 @@ void DeviceManager::TasksChanged() {
|
||||
|
||||
if (!idx.isValid()) continue;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) continue;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) continue;
|
||||
|
||||
info->task_percentage_ = -1;
|
||||
device_info->task_percentage_ = -1;
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
|
||||
active_tasks_.remove(active_tasks_.key(idx));
|
||||
@@ -900,17 +902,17 @@ void DeviceManager::Unmount(const QModelIndex &idx) {
|
||||
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return;
|
||||
|
||||
if (info->database_id_ != -1 && !info->device_) return;
|
||||
if (device_info->database_id_ != -1 && !device_info->device_) return;
|
||||
|
||||
if (info->device_) {
|
||||
info->unmount_ = true;
|
||||
Disconnect(info, idx);
|
||||
if (device_info->device_) {
|
||||
device_info->unmount_ = true;
|
||||
Disconnect(device_info, idx);
|
||||
}
|
||||
else if (info->BestBackend() && info->BestBackend()->lister_) {
|
||||
info->BestBackend()->lister_->UnmountDeviceAsync(info->BestBackend()->unique_id_);
|
||||
else if (device_info->BestBackend() && device_info->BestBackend()->lister_) {
|
||||
device_info->BestBackend()->lister_->UnmountDeviceAsync(device_info->BestBackend()->unique_id_);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -919,13 +921,13 @@ void DeviceManager::DeviceSongCountUpdated(const int count) {
|
||||
|
||||
Q_UNUSED(count);
|
||||
|
||||
ConnectedDevice *device = qobject_cast<ConnectedDevice*>(sender());
|
||||
if (!device) return;
|
||||
ConnectedDevice *connected_device = qobject_cast<ConnectedDevice*>(sender());
|
||||
if (!connected_device) return;
|
||||
|
||||
DeviceInfo *info = FindDeviceById(device->unique_id());
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = FindDeviceById(connected_device->unique_id());
|
||||
if (!device_info) return;
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
@@ -934,10 +936,10 @@ void DeviceManager::DeviceSongCountUpdated(const int count) {
|
||||
|
||||
QString DeviceManager::DeviceNameByID(const QString &unique_id) {
|
||||
|
||||
DeviceInfo *info = FindDeviceById(unique_id);
|
||||
if (!info) return QString();
|
||||
DeviceInfo *device_info = FindDeviceById(unique_id);
|
||||
if (!device_info) return QString();
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return QString();
|
||||
|
||||
return data(idx, DeviceManager::Role_FriendlyName).toString();
|
||||
|
||||
@@ -85,11 +85,11 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
||||
LastRole,
|
||||
};
|
||||
|
||||
enum State {
|
||||
State_Remembered,
|
||||
State_NotMounted,
|
||||
State_NotConnected,
|
||||
State_Connected,
|
||||
enum class State {
|
||||
Remembered,
|
||||
NotMounted,
|
||||
NotConnected,
|
||||
Connected,
|
||||
};
|
||||
|
||||
static const int kDeviceIconSize;
|
||||
@@ -104,17 +104,17 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
||||
DeviceLister *GetLister(const QModelIndex &idx) const;
|
||||
DeviceInfo *GetDevice(const QModelIndex &idx) const;
|
||||
SharedPtr<ConnectedDevice> GetConnectedDevice(const QModelIndex &idx) const;
|
||||
SharedPtr<ConnectedDevice> GetConnectedDevice(DeviceInfo *info) const;
|
||||
SharedPtr<ConnectedDevice> GetConnectedDevice(DeviceInfo *device_info) const;
|
||||
|
||||
DeviceInfo *FindDeviceById(const QString &id) const;
|
||||
DeviceInfo *FindDeviceByUrl(const QList<QUrl> &url) const;
|
||||
QString DeviceNameByID(const QString &unique_id);
|
||||
DeviceInfo *FindEquivalentDevice(DeviceInfo *info) const;
|
||||
DeviceInfo *FindEquivalentDevice(const QStringList &unique_ids) const;
|
||||
|
||||
// Actions on devices
|
||||
SharedPtr<ConnectedDevice> Connect(DeviceInfo *info);
|
||||
SharedPtr<ConnectedDevice> Connect(DeviceInfo *device_info);
|
||||
SharedPtr<ConnectedDevice> Connect(const QModelIndex &idx);
|
||||
void Disconnect(DeviceInfo *info, const QModelIndex &idx);
|
||||
void Disconnect(DeviceInfo *device_info, const QModelIndex &idx);
|
||||
void Forget(const QModelIndex &idx);
|
||||
void UnmountAsync(const QModelIndex &idx);
|
||||
|
||||
@@ -128,9 +128,10 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
||||
|
||||
Q_SIGNALS:
|
||||
void ExitFinished();
|
||||
void DevicesLoaded(const DeviceDatabaseBackend::DeviceList &devices);
|
||||
void DeviceConnected(const QModelIndex idx);
|
||||
void DeviceDisconnected(const QModelIndex idx);
|
||||
void DeviceCreatedFromDB(DeviceInfo *info);
|
||||
void DeviceCreatedFromDB(DeviceInfo *device_info);
|
||||
void DeviceError(const QString &error);
|
||||
|
||||
private Q_SLOTS:
|
||||
@@ -143,7 +144,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
||||
void LoadAllDevices();
|
||||
void DeviceConnectFinished(const QString &id, bool success);
|
||||
void DeviceCloseFinished(const QString &id);
|
||||
void AddDeviceFromDB(DeviceInfo *info);
|
||||
void AddDevicesFromDB(const DeviceDatabaseBackend::DeviceList &devices);
|
||||
void BackendClosed();
|
||||
void ListerClosed();
|
||||
void DeviceDestroyed();
|
||||
@@ -154,7 +155,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
||||
|
||||
DeviceDatabaseBackend::Device InfoToDatabaseDevice(const DeviceInfo &info) const;
|
||||
|
||||
void RemoveFromDB(DeviceInfo *info, const QModelIndex &idx);
|
||||
void RemoveFromDB(DeviceInfo *device_info, const QModelIndex &idx);
|
||||
|
||||
void CloseDevices();
|
||||
void CloseListers();
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#include "devicemanager.h"
|
||||
#include "devicestatefiltermodel.h"
|
||||
|
||||
DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::State state)
|
||||
DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, const DeviceManager::State state)
|
||||
: QSortFilterProxyModel(parent),
|
||||
state_(state) {
|
||||
|
||||
@@ -40,7 +40,7 @@ DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::S
|
||||
|
||||
bool DeviceStateFilterModel::filterAcceptsRow(const int row, const QModelIndex &parent) const {
|
||||
Q_UNUSED(parent)
|
||||
return sourceModel()->index(row, 0).data(DeviceManager::Role_State).toInt() != state_ && sourceModel()->index(row, 0).data(DeviceManager::Role_CopyMusic).toBool();
|
||||
return sourceModel()->index(row, 0).data(DeviceManager::Role_State).value<DeviceManager::State>() != state_ && sourceModel()->index(row, 0).data(DeviceManager::Role_CopyMusic).toBool();
|
||||
}
|
||||
|
||||
void DeviceStateFilterModel::ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last) {
|
||||
|
||||
@@ -37,7 +37,7 @@ class DeviceStateFilterModel : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeviceStateFilterModel(QObject *parent, DeviceManager::State state = DeviceManager::State_Remembered);
|
||||
explicit DeviceStateFilterModel(QObject *parent, const DeviceManager::State state = DeviceManager::State::Remembered);
|
||||
|
||||
void setSourceModel(QAbstractItemModel *sourceModel) override;
|
||||
|
||||
@@ -52,7 +52,7 @@ class DeviceStateFilterModel : public QSortFilterProxyModel {
|
||||
void ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last);
|
||||
|
||||
private:
|
||||
DeviceManager::State state_;
|
||||
const DeviceManager::State state_;
|
||||
};
|
||||
|
||||
#endif // DEVICESTATEFILTERMODEL_H
|
||||
|
||||
@@ -128,19 +128,19 @@ void DeviceItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
|
||||
}
|
||||
else {
|
||||
switch (state) {
|
||||
case DeviceManager::State_Remembered:
|
||||
case DeviceManager::State::Remembered:
|
||||
status_text = tr("Not connected");
|
||||
break;
|
||||
|
||||
case DeviceManager::State_NotMounted:
|
||||
case DeviceManager::State::NotMounted:
|
||||
status_text = tr("Not mounted - double click to mount");
|
||||
break;
|
||||
|
||||
case DeviceManager::State_NotConnected:
|
||||
case DeviceManager::State::NotConnected:
|
||||
status_text = tr("Double click to open");
|
||||
break;
|
||||
|
||||
case DeviceManager::State_Connected:{
|
||||
case DeviceManager::State::Connected:{
|
||||
QVariant song_count = idx.data(DeviceManager::Role_SongCount);
|
||||
if (song_count.isValid()) {
|
||||
int count = song_count.toInt();
|
||||
|
||||
@@ -409,5 +409,6 @@ bool GPodDevice::FinishDelete(bool success, QString &error_text) {
|
||||
bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
|
||||
*ret << Song::FileType::MP4;
|
||||
*ret << Song::FileType::MPEG;
|
||||
*ret << Song::FileType::ALAC;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -60,6 +60,7 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
|
||||
void UpdateDeviceFreeSpace(const QString &id);
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
struct MTPDevice {
|
||||
MTPDevice() : capacity(0), free_space(0) {}
|
||||
QString vendor;
|
||||
@@ -74,6 +75,7 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
quint64 capacity;
|
||||
quint64 free_space;
|
||||
};
|
||||
#endif // HAVE_MTP
|
||||
|
||||
void ExitAsync();
|
||||
|
||||
@@ -91,11 +93,12 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
|
||||
static void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context);
|
||||
|
||||
void FoundMTPDevice(const MTPDevice &device, const QString &serial);
|
||||
#ifdef HAVE_MTP
|
||||
void FoundMTPDevice(const MTPDevice &mtp_device, const QString &serial);
|
||||
void RemovedMTPDevice(const QString &serial);
|
||||
|
||||
quint64 GetFreeSpace(const QUrl &url);
|
||||
quint64 GetCapacity(const QUrl &url);
|
||||
#endif // HAVE_MTP
|
||||
|
||||
bool IsCDDevice(const QString &serial) const;
|
||||
|
||||
@@ -103,18 +106,23 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
CFRunLoopRef run_loop_;
|
||||
|
||||
QMap<QString, QString> current_devices_;
|
||||
#ifdef HAVE_MTP
|
||||
QMap<QString, MTPDevice> mtp_devices_;
|
||||
#endif
|
||||
QSet<QString> cd_devices_;
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
QMutex libmtp_mutex_;
|
||||
|
||||
static QSet<MTPDevice> sMTPDeviceList;
|
||||
#endif
|
||||
};
|
||||
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &device);
|
||||
#ifdef HAVE_MTP
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &mtp_device);
|
||||
|
||||
inline bool operator==(const MacOsDeviceLister::MTPDevice &a, const MacOsDeviceLister::MTPDevice &b) {
|
||||
return (a.vendor_id == b.vendor_id) && (a.product_id == b.product_id);
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
#endif // MACDEVICELISTER_H
|
||||
|
||||
@@ -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,7 +21,9 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <libmtp.h>
|
||||
#ifdef HAVE_MTP
|
||||
# include <libmtp.h>
|
||||
#endif
|
||||
|
||||
#include <AvailabilityMacros.h>
|
||||
#include <CoreFoundation/CFRunLoop.h>
|
||||
@@ -41,12 +43,15 @@
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "macosdevicelister.h"
|
||||
#include "mtpconnection.h"
|
||||
#include "includes/scoped_cftyperef.h"
|
||||
#include "includes/scoped_nsobject.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/scoped_nsautorelease_pool.h"
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
# include "mtpconnection.h"
|
||||
#endif
|
||||
|
||||
#import <AppKit/NSWorkspace.h>
|
||||
#import <Foundation/NSDictionary.h>
|
||||
#import <Foundation/NSNotification.h>
|
||||
@@ -102,11 +107,15 @@ class ScopedIOObject {
|
||||
// Libgphoto2 MTP detection code:
|
||||
// http://www.sfr-fresh.com/unix/privat/libgphoto2-2.4.10.1.tar.gz:a/libgphoto2-2.4.10.1/libgphoto2_port/usb/check-mtp-device.c
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
QSet<MacOsDeviceLister::MTPDevice> MacOsDeviceLister::sMTPDeviceList;
|
||||
#endif
|
||||
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &d) {
|
||||
return qHash(d.vendor_id) ^ qHash(d.product_id);
|
||||
#ifdef HAVE_MTP
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &mtp_device) {
|
||||
return qHash(mtp_device.vendor_id) ^ qHash(mtp_device.product_id);
|
||||
}
|
||||
#endif
|
||||
|
||||
MacOsDeviceLister::MacOsDeviceLister(QObject *parent) : DeviceLister(parent) {}
|
||||
|
||||
@@ -116,6 +125,7 @@ bool MacOsDeviceLister::Init() {
|
||||
|
||||
ScopedNSAutoreleasePool pool;
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
// Populate MTP Device list.
|
||||
if (sMTPDeviceList.empty()) {
|
||||
LIBMTP_device_entry_t *devices = nullptr;
|
||||
@@ -126,25 +136,26 @@ bool MacOsDeviceLister::Init() {
|
||||
else {
|
||||
for (int i = 0; i < num; ++i) {
|
||||
LIBMTP_device_entry_t device = devices[i];
|
||||
MTPDevice d;
|
||||
d.vendor = QString::fromLatin1(device.vendor);
|
||||
d.vendor_id = device.vendor_id;
|
||||
d.product = QString::fromLatin1(device.product);
|
||||
d.product_id = device.product_id;
|
||||
d.quirks = device.device_flags;
|
||||
sMTPDeviceList << d;
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = QString::fromLatin1(device.vendor);
|
||||
mtp_device.vendor_id = device.vendor_id;
|
||||
mtp_device.product = QString::fromLatin1(device.product);
|
||||
mtp_device.product_id = device.product_id;
|
||||
mtp_device.quirks = device.device_flags;
|
||||
sMTPDeviceList << mtp_device;
|
||||
}
|
||||
}
|
||||
|
||||
MTPDevice d;
|
||||
d.vendor = "SanDisk"_L1;
|
||||
d.vendor_id = 0x781;
|
||||
d.product = "Sansa Clip+"_L1;
|
||||
d.product_id = 0x74d0;
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = "SanDisk"_L1;
|
||||
mtp_device.vendor_id = 0x781;
|
||||
mtp_device.product = "Sansa Clip+"_L1;
|
||||
mtp_device.product_id = 0x74d0;
|
||||
|
||||
d.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
|
||||
sMTPDeviceList << d;
|
||||
mtp_device.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
|
||||
sMTPDeviceList << mtp_device;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
run_loop_ = CFRunLoopGetCurrent();
|
||||
|
||||
@@ -240,12 +251,14 @@ CFTypeRef GetUSBRegistryEntry(io_object_t device, CFStringRef key) {
|
||||
}
|
||||
|
||||
QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) {
|
||||
|
||||
ScopedCFTypeRef<CFStringRef> registry_string(reinterpret_cast<CFStringRef>(GetUSBRegistryEntry(device, key)));
|
||||
if (registry_string) {
|
||||
return QString::fromUtf8([reinterpret_cast<NSString*>(registry_string.get()) UTF8String]);
|
||||
}
|
||||
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
|
||||
@@ -277,17 +290,13 @@ NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
|
||||
|
||||
int GetUSBDeviceClass(io_object_t device) {
|
||||
|
||||
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(
|
||||
device,
|
||||
kIOServicePlane,
|
||||
CFSTR(kUSBInterfaceClass),
|
||||
kCFAllocatorDefault,
|
||||
kIORegistryIterateRecursively));
|
||||
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(device, kIOServicePlane, CFSTR(kUSBInterfaceClass), kCFAllocatorDefault, kIORegistryIterateRecursively));
|
||||
NSNumber *number = reinterpret_cast<NSNumber*>(interface_class.get());
|
||||
if (number) {
|
||||
int ret = [number unsignedShortValue];
|
||||
return ret;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
@@ -322,12 +331,14 @@ QString GetSerialForDevice(io_object_t device) {
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
QString GetSerialForMTPDevice(io_object_t device) {
|
||||
|
||||
scoped_nsobject<NSString> serial(reinterpret_cast<NSString*>(GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString))));
|
||||
return "MTP/"_L1 + QString::fromUtf8([serial UTF8String]);
|
||||
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
|
||||
|
||||
@@ -343,6 +354,7 @@ QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
quint64 MacOsDeviceLister::GetFreeSpace(const QUrl &url) {
|
||||
|
||||
QMutexLocker l(&libmtp_mutex_);
|
||||
@@ -380,6 +392,8 @@ quint64 MacOsDeviceLister::GetCapacity(const QUrl &url) {
|
||||
|
||||
}
|
||||
|
||||
#endif // HAVE_MTP
|
||||
|
||||
void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
|
||||
@@ -390,12 +404,12 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
NSString *kind = [properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionMediaKindKey)];
|
||||
if (kind && strcmp([kind UTF8String], kIOCDMediaClass) == 0) {
|
||||
// CD inserted.
|
||||
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
me->cd_devices_ << bsd_name;
|
||||
Q_EMIT me->DeviceAdded(bsd_name);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#endif // HAVE_AUDIOCD
|
||||
|
||||
NSURL *volume_path = [[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy];
|
||||
|
||||
@@ -403,8 +417,8 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk));
|
||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(device.get()));
|
||||
if (class_name && CFStringCompare(class_name.get(), CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) {
|
||||
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
|
||||
CFMutableDictionaryRef cf_properties;
|
||||
kern_return_t ret = IORegistryEntryCreateCFProperties(device.get(), &cf_properties, kCFAllocatorDefault, 0);
|
||||
@@ -412,7 +426,7 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
if (ret == KERN_SUCCESS) {
|
||||
scoped_nsobject<NSDictionary> dict(reinterpret_cast<NSDictionary*>(cf_properties)); // Takes ownership.
|
||||
if ([[dict objectForKey:@"Removable"] intValue] == 1) {
|
||||
QString serial = GetSerialForDevice(device.get());
|
||||
const QString serial = GetSerialForDevice(device.get());
|
||||
if (!serial.isEmpty()) {
|
||||
me->current_devices_[serial] = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
Q_EMIT me->DeviceAdded(serial);
|
||||
@@ -427,10 +441,9 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
void MacOsDeviceLister::DiskRemovedCallback(DADiskRef disk, void *context) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
|
||||
// We cannot access the USB tree when the disk is removed but we still get
|
||||
// the BSD disk name.
|
||||
// We cannot access the USB tree when the disk is removed but we still get the BSD disk name.
|
||||
|
||||
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
if (me->cd_devices_.remove(bsd_name)) {
|
||||
Q_EMIT me->DeviceRemoved(bsd_name);
|
||||
return;
|
||||
@@ -496,6 +509,7 @@ int GetBusNumber(io_object_t o) {
|
||||
void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
|
||||
Q_UNUSED(me)
|
||||
|
||||
io_object_t object;
|
||||
while ((object = IOIteratorNext(it))) {
|
||||
@@ -503,30 +517,34 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
|
||||
|
||||
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
||||
|
||||
const int interface_class = GetUSBDeviceClass(object);
|
||||
qLog(Debug) << "Interface class:" << interface_class;
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
|
||||
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
|
||||
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
|
||||
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
|
||||
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
|
||||
int interface_class = GetUSBDeviceClass(object);
|
||||
qLog(Debug) << "Interface class:" << interface_class;
|
||||
|
||||
QString serial = GetSerialForMTPDevice(object);
|
||||
const QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice device;
|
||||
device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
device.product = QString::fromUtf8([product UTF8String]);
|
||||
device.vendor_id = [vendor_id unsignedShortValue];
|
||||
device.product_id = [product_id unsignedShortValue];
|
||||
device.quirks = 0;
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
mtp_device.product = QString::fromUtf8([product UTF8String]);
|
||||
mtp_device.vendor_id = [vendor_id unsignedShortValue];
|
||||
mtp_device.product_id = [product_id unsignedShortValue];
|
||||
mtp_device.quirks = 0;
|
||||
|
||||
device.bus = -1;
|
||||
device.address = -1;
|
||||
mtp_device.bus = -1;
|
||||
mtp_device.address = -1;
|
||||
|
||||
if (device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products.
|
||||
if (mtp_device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products.
|
||||
// Blacklist ilok2 as this probe may be breaking it.
|
||||
(device.vendor_id == 0x088e && device.product_id == 0x5036) ||
|
||||
(mtp_device.vendor_id == 0x088e && mtp_device.product_id == 0x5036) ||
|
||||
// Blacklist eLicenser
|
||||
(device.vendor_id == 0x0819 && device.product_id == 0x0101) ||
|
||||
(mtp_device.vendor_id == 0x0819 && mtp_device.product_id == 0x0101) ||
|
||||
// Skip HID devices, printers and hubs.
|
||||
interface_class == kUSBHIDInterfaceClass ||
|
||||
interface_class == kUSBPrintingInterfaceClass ||
|
||||
@@ -535,31 +553,28 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
}
|
||||
|
||||
NSNumber *addr = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR("USB Address")));
|
||||
int bus = GetBusNumber(object);
|
||||
const int bus = GetBusNumber(object);
|
||||
if (!addr || bus == -1) {
|
||||
// Failed to get bus or address number.
|
||||
continue;
|
||||
}
|
||||
device.bus = bus;
|
||||
device.address = [addr intValue];
|
||||
mtp_device.bus = bus;
|
||||
mtp_device.address = [addr intValue];
|
||||
|
||||
// First check the libmtp device list.
|
||||
QSet<MTPDevice>::const_iterator it2 = sMTPDeviceList.find(device);
|
||||
QSet<MTPDevice>::const_iterator it2 = sMTPDeviceList.find(mtp_device);
|
||||
if (it2 != sMTPDeviceList.end()) {
|
||||
// Fill in quirks flags from libmtp.
|
||||
device.quirks = it2->quirks;
|
||||
me->FoundMTPDevice(device, GetSerialForMTPDevice(object));
|
||||
mtp_device.quirks = it2->quirks;
|
||||
me->FoundMTPDevice(mtp_device, GetSerialForMTPDevice(object));
|
||||
continue;
|
||||
}
|
||||
|
||||
#endif // HAVE_MTP
|
||||
|
||||
IOCFPlugInInterface **plugin_interface = nullptr;
|
||||
SInt32 score;
|
||||
kern_return_t err = IOCreatePlugInInterfaceForService(
|
||||
object,
|
||||
kIOUSBDeviceUserClientTypeID,
|
||||
kIOCFPlugInInterfaceID,
|
||||
&plugin_interface,
|
||||
&score);
|
||||
kern_return_t err = IOCreatePlugInInterfaceForService(object, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin_interface, &score);
|
||||
if (err != KERN_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
@@ -590,7 +605,7 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
bool ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, 2, &data);
|
||||
if (!ret) continue;
|
||||
|
||||
UInt8 string_len = data[0];
|
||||
const UInt8 string_len = data[0];
|
||||
|
||||
ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, string_len, &data);
|
||||
if (!ret) continue;
|
||||
@@ -599,6 +614,7 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
// Because this was designed by MS, the characters are in UTF-16 (LE?).
|
||||
QString str = QString::fromUtf16(reinterpret_cast<char16_t*>(data.data() + 2), (data.size() / 2) - 2);
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (str.startsWith("MSFT100"_L1)) {
|
||||
// We got the OS descriptor!
|
||||
char vendor_code = data[16];
|
||||
@@ -621,8 +637,10 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
continue;
|
||||
}
|
||||
// Hurray! We made it!
|
||||
me->FoundMTPDevice(device, serial);
|
||||
me->FoundMTPDevice(mtp_device, serial);
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,30 +649,39 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
void MacOsDeviceLister::USBDeviceRemovedCallback(void *refcon, io_iterator_t it) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
|
||||
Q_UNUSED(me)
|
||||
|
||||
io_object_t object;
|
||||
while ((object = IOIteratorNext(it))) {
|
||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object));
|
||||
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
|
||||
|
||||
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
|
||||
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
|
||||
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
|
||||
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
|
||||
QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice device;
|
||||
device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
device.product = QString::fromUtf8([product UTF8String]);
|
||||
device.vendor_id = [vendor_id unsignedShortValue];
|
||||
device.product_id = [product_id unsignedShortValue];
|
||||
const QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
mtp_device.product = QString::fromUtf8([product UTF8String]);
|
||||
mtp_device.vendor_id = [vendor_id unsignedShortValue];
|
||||
mtp_device.product_id = [product_id unsignedShortValue];
|
||||
|
||||
me->RemovedMTPDevice(serial);
|
||||
#endif // HAVE_MTP
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
|
||||
void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
|
||||
|
||||
int count = mtp_devices_.remove(serial);
|
||||
@@ -668,34 +695,40 @@ void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
|
||||
void MacOsDeviceLister::FoundMTPDevice(const MTPDevice &device, const QString &serial) {
|
||||
|
||||
qLog(Debug) << "New MTP device detected!" << device.bus << device.address;
|
||||
|
||||
mtp_devices_[serial] = device;
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice *d = &mtp_devices_[serial];
|
||||
d->capacity = GetCapacity(urls[0]);
|
||||
d->free_space = GetFreeSpace(urls[0]);
|
||||
const QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice *mtp_device = &mtp_devices_[serial];
|
||||
mtp_device->capacity = GetCapacity(urls[0]);
|
||||
mtp_device->free_space = GetFreeSpace(urls[0]);
|
||||
|
||||
Q_EMIT DeviceAdded(serial);
|
||||
|
||||
}
|
||||
|
||||
bool IsMTPSerial(const QString &serial) { return serial.startsWith("MTP"_L1); }
|
||||
|
||||
#endif // HAVE_MTP
|
||||
|
||||
bool MacOsDeviceLister::IsCDDevice(const QString &serial) const {
|
||||
return cd_devices_.contains(serial);
|
||||
}
|
||||
|
||||
QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
const MTPDevice &device = mtp_devices_[serial];
|
||||
if (device.vendor.isEmpty()) {
|
||||
return device.product;
|
||||
const MTPDevice &mtp_device = mtp_devices_[serial];
|
||||
if (mtp_device.vendor.isEmpty()) {
|
||||
return mtp_device.product;
|
||||
}
|
||||
else {
|
||||
return device.vendor + QLatin1Char(' ') + device.product;
|
||||
return mtp_device.vendor + QLatin1Char(' ') + mtp_device.product;
|
||||
}
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial];
|
||||
const QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
@@ -708,75 +741,86 @@ QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
|
||||
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk));
|
||||
|
||||
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
|
||||
if (vendor.isEmpty()) {
|
||||
return product;
|
||||
}
|
||||
|
||||
return vendor + QLatin1Char(' ') + product;
|
||||
|
||||
}
|
||||
|
||||
QList<QUrl> MacOsDeviceLister::MakeDeviceUrls(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
const MTPDevice &device = mtp_devices_[serial];
|
||||
QString str = QString::asprintf("gphoto2://usb-%d-%d/", device.bus, device.address);
|
||||
const MTPDevice &mtp_device = mtp_devices_[serial];
|
||||
const QString str = QString::asprintf("gphoto2://usb-%d-%d/", mtp_device.bus, mtp_device.address);
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem(u"vendor"_s, device.vendor);
|
||||
url_query.addQueryItem(u"vendor_id"_s, QString::number(device.vendor_id));
|
||||
url_query.addQueryItem(u"product"_s, device.product);
|
||||
url_query.addQueryItem(u"product_id"_s, QString::number(device.product_id));
|
||||
url_query.addQueryItem(u"quirks"_s, QString::number(device.quirks));
|
||||
url_query.addQueryItem(u"vendor"_s, mtp_device.vendor);
|
||||
url_query.addQueryItem(u"vendor_id"_s, QString::number(mtp_device.vendor_id));
|
||||
url_query.addQueryItem(u"product"_s, mtp_device.product);
|
||||
url_query.addQueryItem(u"product_id"_s, QString::number(mtp_device.product_id));
|
||||
url_query.addQueryItem(u"quirks"_s, QString::number(mtp_device.quirks));
|
||||
QUrl url(str);
|
||||
url.setQuery(url_query);
|
||||
return QList<QUrl>() << url;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
if (IsCDDevice(serial)) {
|
||||
return QList<QUrl>() << QUrl(u"cdda:///dev/r"_s + serial);
|
||||
}
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk.get())));
|
||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
|
||||
|
||||
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
QUrl ret = MakeUrlFromLocalPath(path);
|
||||
const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
const QUrl ret = MakeUrlFromLocalPath(path);
|
||||
|
||||
return QList<QUrl>() << ret;
|
||||
|
||||
}
|
||||
|
||||
QStringList MacOsDeviceLister::DeviceUniqueIDs() {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
return current_devices_.keys() + mtp_devices_.keys();
|
||||
#else
|
||||
return current_devices_.keys();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
return QVariantList();
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
if (IsCDDevice(serial)) {
|
||||
return QVariantList() << u"media-optical"_s;
|
||||
}
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk.get()));
|
||||
QString icon = GetIconForDevice(device.get());
|
||||
const QString icon = GetIconForDevice(device.get());
|
||||
|
||||
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk)));
|
||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
|
||||
|
||||
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
|
||||
QVariantList ret;
|
||||
ret << GuessIconForPath(path);
|
||||
@@ -784,31 +828,45 @@ QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
|
||||
if (!icon.isEmpty()) {
|
||||
ret << icon;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QString MacOsDeviceLister::DeviceManufacturer(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
return mtp_devices_[serial].vendor;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString));
|
||||
|
||||
}
|
||||
|
||||
QString MacOsDeviceLister::DeviceModel(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
return mtp_devices_[serial].product;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString));
|
||||
|
||||
}
|
||||
|
||||
quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
return mtp_devices_[serial].capacity;
|
||||
}
|
||||
QString bsd_name = current_devices_[serial];
|
||||
#endif // HAVE_MTP
|
||||
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
@@ -816,7 +874,7 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
||||
|
||||
NSNumber *capacity = reinterpret_cast<NSNumber*>(GetPropertyForDevice(device, CFSTR("Size")));
|
||||
|
||||
quint64 ret = [capacity unsignedLongLongValue];
|
||||
const quint64 ret = [capacity unsignedLongLongValue];
|
||||
|
||||
IOObjectRelease(device);
|
||||
|
||||
@@ -826,10 +884,13 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
||||
|
||||
quint64 MacOsDeviceLister::DeviceFreeSpace(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
return mtp_devices_[serial].free_space;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
@@ -857,9 +918,11 @@ bool MacOsDeviceLister::AskForScan(const QString &serial) const {
|
||||
|
||||
void MacOsDeviceLister::UnmountDevice(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) return;
|
||||
#endif
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, loop_session_, bsd_name.toLatin1().constData()));
|
||||
|
||||
DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this);
|
||||
@@ -879,13 +942,16 @@ void MacOsDeviceLister::DiskUnmountCallback(DADiskRef disk, DADissenterRef disse
|
||||
|
||||
void MacOsDeviceLister::UpdateDeviceFreeSpace(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
if (mtp_devices_.contains(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice *d = &mtp_devices_[serial];
|
||||
d->free_space = GetFreeSpace(urls[0]);
|
||||
MTPDevice *mtp_device = &mtp_devices_[serial];
|
||||
mtp_device->free_space = GetFreeSpace(urls[0]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Q_EMIT DeviceChanged(serial);
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user