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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
opensuse_version: [ 'tumbleweed', 'leap:15.6' ]
|
opensuse_version: [ 'tumbleweed', 'leap:15.6', 'leap:16.0' ]
|
||||||
container:
|
container:
|
||||||
image: opensuse/${{matrix.opensuse_version}}
|
image: opensuse/${{matrix.opensuse_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -27,11 +27,11 @@ jobs:
|
|||||||
- name: Upgrade packages (Leap)
|
- name: Upgrade packages (Leap)
|
||||||
if: matrix.opensuse_version != 'tumbleweed'
|
if: matrix.opensuse_version != 'tumbleweed'
|
||||||
run: zypper -n --gpg-auto-import-keys up
|
run: zypper -n --gpg-auto-import-keys up
|
||||||
- name: Install gcc (Tumbleweed)
|
- name: Install gcc
|
||||||
if: matrix.opensuse_version == 'tumbleweed'
|
if: matrix.opensuse_version != 'leap:15.6'
|
||||||
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
|
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
|
||||||
- name: Install gcc (Leap)
|
- name: Install gcc (leap:15.6)
|
||||||
if: matrix.opensuse_version != 'tumbleweed'
|
if: matrix.opensuse_version == 'leap:15.6'
|
||||||
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
|
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: >
|
run: >
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
sparsehash-devel
|
sparsehash-devel
|
||||||
rapidjson-devel
|
rapidjson-devel
|
||||||
- name: Install kdsingleapplication-qt6-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
|
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
|
||||||
- name: Build and install KDSingleApplication
|
- name: Build and install KDSingleApplication
|
||||||
if: matrix.opensuse_version == 'leap:15.6'
|
if: matrix.opensuse_version == 'leap:15.6'
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
cmake --build build --config Release --parallel 4
|
cmake --build build --config Release --parallel 4
|
||||||
cmake --install build
|
cmake --install build
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -115,14 +115,14 @@ jobs:
|
|||||||
- name: Copy source tarball
|
- name: Copy source tarball
|
||||||
working-directory: build
|
working-directory: build
|
||||||
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
|
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
|
||||||
- name: Build RPM (Tumbleweed)
|
- name: Build RPM
|
||||||
if: matrix.opensuse_version == 'tumbleweed'
|
if: matrix.opensuse_version != 'leap:15.6'
|
||||||
env:
|
env:
|
||||||
RPM_BUILD_NCPUS: 4
|
RPM_BUILD_NCPUS: 4
|
||||||
working-directory: build
|
working-directory: build
|
||||||
run: rpmbuild -ba strawberry.spec
|
run: rpmbuild -ba strawberry.spec
|
||||||
- name: Build RPM (Leap)
|
- name: Build RPM (leap:15.6)
|
||||||
if: matrix.opensuse_version != 'tumbleweed'
|
if: matrix.opensuse_version == 'leap:15.6'
|
||||||
env:
|
env:
|
||||||
RPM_BUILD_NCPUS: 4
|
RPM_BUILD_NCPUS: 4
|
||||||
CC: gcc-14
|
CC: gcc-14
|
||||||
@@ -156,7 +156,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
fedora_version: [ '39', '40', '41', '42' ]
|
fedora_version: [ '41', '42', '43' ]
|
||||||
container:
|
container:
|
||||||
image: fedora:${{matrix.fedora_version}}
|
image: fedora:${{matrix.fedora_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -209,7 +209,7 @@ jobs:
|
|||||||
sparsehash-devel
|
sparsehash-devel
|
||||||
rapidjson-devel
|
rapidjson-devel
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -307,7 +307,7 @@ jobs:
|
|||||||
- name: Remove files
|
- name: Remove files
|
||||||
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
|
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -409,7 +409,7 @@ jobs:
|
|||||||
cmake --build build --config Release --parallel 4
|
cmake --build build --config Release --parallel 4
|
||||||
cmake --install build
|
cmake --install build
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -449,7 +449,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
debian_version: [ 'bookworm', 'trixie' ]
|
debian_version: [ 'bookworm', 'trixie', 'forky' ]
|
||||||
container:
|
container:
|
||||||
image: debian:${{matrix.debian_version}}
|
image: debian:${{matrix.debian_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -499,7 +499,11 @@ jobs:
|
|||||||
qt6-tools-dev-tools
|
qt6-tools-dev-tools
|
||||||
qt6-l10n-tools
|
qt6-l10n-tools
|
||||||
rapidjson-dev
|
rapidjson-dev
|
||||||
|
- name: Install KDSingleApplication
|
||||||
|
if: matrix.debian_version != 'bookworm'
|
||||||
|
run: apt install -y libkdsingleapplication-qt6-dev
|
||||||
- name: Build and install KDSingleApplication
|
- name: Build and install KDSingleApplication
|
||||||
|
if: matrix.debian_version == 'bookworm'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||||
cd KDSingleApplication
|
cd KDSingleApplication
|
||||||
@@ -507,7 +511,7 @@ jobs:
|
|||||||
cmake --build build --config Release --parallel 4
|
cmake --build build --config Release --parallel 4
|
||||||
cmake --install build
|
cmake --install build
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -538,7 +542,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ubuntu_version: [ 'noble', 'oracular' ]
|
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
|
||||||
container:
|
container:
|
||||||
image: ubuntu:${{matrix.ubuntu_version}}
|
image: ubuntu:${{matrix.ubuntu_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -591,7 +595,11 @@ jobs:
|
|||||||
qt6-tools-dev-tools
|
qt6-tools-dev-tools
|
||||||
qt6-l10n-tools
|
qt6-l10n-tools
|
||||||
rapidjson-dev
|
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
|
- name: Build and install KDSingleApplication
|
||||||
|
if: matrix.ubuntu_version == 'noble' || matrix.ubuntu_version == 'plucky'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||||
cd KDSingleApplication
|
cd KDSingleApplication
|
||||||
@@ -599,7 +607,7 @@ jobs:
|
|||||||
cmake --build build --config Release --parallel 4
|
cmake --build build --config Release --parallel 4
|
||||||
cmake --install build
|
cmake --install build
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -631,7 +639,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ubuntu_version: [ 'noble', 'oracular' ]
|
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
|
||||||
container:
|
container:
|
||||||
image: ubuntu:${{matrix.ubuntu_version}}
|
image: ubuntu:${{matrix.ubuntu_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -691,7 +699,7 @@ jobs:
|
|||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
run: apt install -y keyboxd
|
run: apt install -y keyboxd
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -727,13 +735,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Build FreeBSD
|
- name: Build FreeBSD
|
||||||
id: build-freebsd
|
id: build-freebsd
|
||||||
uses: vmactions/freebsd-vm@v1.1.9
|
uses: vmactions/freebsd-vm@v1.2.4
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
mem: 4096
|
mem: 4096
|
||||||
@@ -752,13 +760,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Build OpenBSD
|
- name: Build OpenBSD
|
||||||
id: build-openbsd
|
id: build-openbsd
|
||||||
uses: vmactions/openbsd-vm@v1.1.6
|
uses: vmactions/openbsd-vm@v1.2.1
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
mem: 4096
|
mem: 4096
|
||||||
@@ -788,7 +796,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set MACOSX_DEPLOYMENT_TARGET
|
- name: Set MACOSX_DEPLOYMENT_TARGET
|
||||||
run: |
|
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
|
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
|
||||||
echo "Using macOS SDK ${i}"
|
echo "Using macOS SDK ${i}"
|
||||||
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
|
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
|
||||||
@@ -818,7 +826,7 @@ jobs:
|
|||||||
rm -f uninstall.sh
|
rm -f uninstall.sh
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -946,7 +954,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set MACOSX_DEPLOYMENT_TARGET
|
- name: Set MACOSX_DEPLOYMENT_TARGET
|
||||||
run: |
|
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
|
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
|
||||||
echo "Using macOS SDK ${i}"
|
echo "Using macOS SDK ${i}"
|
||||||
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
|
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
|
run: echo "cmake_buildtype=$(echo ${{env.buildtype}} | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}')" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -1072,7 +1080,7 @@ jobs:
|
|||||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -1246,12 +1254,42 @@ jobs:
|
|||||||
build-windows-msvc:
|
build-windows-msvc:
|
||||||
name: Build Windows MSVC
|
name: Build Windows MSVC
|
||||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
||||||
runs-on: windows-2022
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ 'x86', 'x86_64' ]
|
include:
|
||||||
buildtype: [ 'release' ]
|
- 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:
|
steps:
|
||||||
|
|
||||||
- name: Set prefix path
|
- name: Set prefix path
|
||||||
@@ -1265,10 +1303,28 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
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
|
- name: Install rsync
|
||||||
shell: cmd
|
shell: cmd
|
||||||
run: choco install --no-progress rsync
|
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
|
- name: Cleanup PATH
|
||||||
uses: egor-tensin/cleanup-path@v4
|
uses: egor-tensin/cleanup-path@v4
|
||||||
with:
|
with:
|
||||||
@@ -1293,7 +1349,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Copy bin files
|
- name: Copy bin files
|
||||||
shell: bash
|
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
|
- name: Delete conflicting files
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -1347,11 +1405,11 @@ jobs:
|
|||||||
uses: ilammy/msvc-dev-cmd@v1
|
uses: ilammy/msvc-dev-cmd@v1
|
||||||
with:
|
with:
|
||||||
arch: ${{matrix.arch}}
|
arch: ${{matrix.arch}}
|
||||||
sdk: 10.0.20348.0
|
sdk: ${{env.sdk_version}}
|
||||||
vsversion: 2022
|
vsversion: 2022
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -1364,15 +1422,18 @@ jobs:
|
|||||||
shell: cmd
|
shell: cmd
|
||||||
run: cmake -E make_directory build
|
run: cmake -E make_directory build
|
||||||
|
|
||||||
- name: Set ENABLE_WIN32_CONSOLE (debug)
|
- name: Set ENABLE_WIN32_CONSOLE
|
||||||
if: matrix.buildtype == 'debug'
|
|
||||||
shell: bash
|
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)
|
- name: Set ENABLE_SPOTIFY
|
||||||
if: matrix.buildtype == 'release'
|
|
||||||
shell: bash
|
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
|
- name: Run CMake
|
||||||
shell: cmd
|
shell: cmd
|
||||||
@@ -1384,14 +1445,14 @@ jobs:
|
|||||||
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
|
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
|
||||||
-DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake"
|
-DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake"
|
||||||
-DARCH="${{matrix.arch}}"
|
-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"
|
-DPKG_CONFIG_EXECUTABLE="${{env.prefix_path_forwardslash}}/bin/pkg-config.exe"
|
||||||
-DICU_ROOT="${{env.prefix_path_forwardslash}}"
|
-DICU_ROOT="${{env.prefix_path_forwardslash}}"
|
||||||
-DENABLE_GIO=OFF
|
-DENABLE_GIO=OFF
|
||||||
-DENABLE_AUDIOCD=OFF
|
-DENABLE_AUDIOCD=OFF
|
||||||
-DENABLE_MTP=OFF
|
-DENABLE_MTP=OFF
|
||||||
-DENABLE_GPOD=OFF
|
-DENABLE_GPOD=OFF
|
||||||
-DENABLE_SPOTIFY=ON
|
-DENABLE_SPOTIFY=${{env.enable_spotify}}
|
||||||
|
|
||||||
- name: Run Make
|
- name: Run Make
|
||||||
shell: cmd
|
shell: cmd
|
||||||
@@ -1562,7 +1623,7 @@ jobs:
|
|||||||
- name: rsync
|
- name: rsync
|
||||||
if: steps.set-upload-path.outputs.upload_path != ''
|
if: steps.set-upload-path.outputs.upload_path != ''
|
||||||
shell: bash
|
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:
|
upload:
|
||||||
@@ -1581,11 +1642,11 @@ jobs:
|
|||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
run: sudo apt install -y git rsync
|
run: sudo apt install -y git rsync
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: SSH Setup
|
- name: SSH Setup
|
||||||
@@ -1629,7 +1690,7 @@ jobs:
|
|||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
run: sudo apt install -y git jq gh
|
run: sudo apt install -y git jq gh
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Show release assets
|
- name: Show release assets
|
||||||
@@ -1637,7 +1698,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||||
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
|
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: Add artifacts to release
|
- 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 <cerrno>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@@ -28,28 +51,34 @@ static int MsgFlags = 0;
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
static const char *GetTempPath() {
|
static const char *GetTempPath() {
|
||||||
|
|
||||||
const char *temp = getenv("XDG_RUNTIME_DIR");
|
const char *temp = getenv("XDG_RUNTIME_DIR");
|
||||||
temp = temp ? temp : getenv("TMPDIR");
|
temp = temp ? temp : getenv("TMPDIR");
|
||||||
temp = temp ? temp : getenv("TMP");
|
temp = temp ? temp : getenv("TMP");
|
||||||
temp = temp ? temp : getenv("TEMP");
|
temp = temp ? temp : getenv("TEMP");
|
||||||
temp = temp ? temp : "/tmp";
|
temp = temp ? temp : "/tmp";
|
||||||
|
|
||||||
return temp;
|
return temp;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*static*/ BaseConnection *BaseConnection::Create() {
|
BaseConnection *BaseConnection::Create() {
|
||||||
PipeAddr.sun_family = AF_UNIX;
|
PipeAddr.sun_family = AF_UNIX;
|
||||||
return &Connection;
|
return &Connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) {
|
void BaseConnection::Destroy(BaseConnection *&c) {
|
||||||
auto self = reinterpret_cast<BaseConnectionUnix *>(c);
|
|
||||||
|
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
|
||||||
self->Close();
|
self->Close();
|
||||||
c = nullptr;
|
c = nullptr;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BaseConnection::Open() {
|
bool BaseConnection::Open() {
|
||||||
|
|
||||||
const char *tempPath = GetTempPath();
|
const char *tempPath = GetTempPath();
|
||||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||||
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
if (self->sock == -1) {
|
if (self->sock == -1) {
|
||||||
return false;
|
return false;
|
||||||
@@ -61,8 +90,7 @@ bool BaseConnection::Open() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
|
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
|
||||||
snprintf(
|
snprintf(PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
|
||||||
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));
|
int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr));
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
self->isOpen = true;
|
self->isOpen = true;
|
||||||
@@ -70,10 +98,13 @@ bool BaseConnection::Open() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self->Close();
|
self->Close();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BaseConnection::Close() {
|
bool BaseConnection::Close() {
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
||||||
if (self->sock == -1) {
|
if (self->sock == -1) {
|
||||||
return false;
|
return false;
|
||||||
@@ -81,11 +112,14 @@ bool BaseConnection::Close() {
|
|||||||
close(self->sock);
|
close(self->sock);
|
||||||
self->sock = -1;
|
self->sock = -1;
|
||||||
self->isOpen = false;
|
self->isOpen = false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BaseConnection::Write(const void *data, size_t length) {
|
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) {
|
if (self->sock == -1) {
|
||||||
return false;
|
return false;
|
||||||
@@ -95,11 +129,14 @@ bool BaseConnection::Write(const void *data, size_t length) {
|
|||||||
if (sentBytes < 0) {
|
if (sentBytes < 0) {
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return sentBytes == static_cast<ssize_t>(length);
|
return sentBytes == static_cast<ssize_t>(length);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BaseConnection::Read(void *data, size_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) {
|
if (self->sock == -1) {
|
||||||
return false;
|
return false;
|
||||||
@@ -115,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
|
|||||||
else if (res == 0) {
|
else if (res == 0) {
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return static_cast<size_t>(res) == length;
|
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 WIN32_LEAN_AND_MEAN
|
||||||
#define NOMCX
|
#define NOMCX
|
||||||
#define NOSERVICE
|
#define NOSERVICE
|
||||||
#define NOIME
|
#define NOIME
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
@@ -19,24 +43,26 @@ struct BaseConnectionWin : public BaseConnection {
|
|||||||
|
|
||||||
static BaseConnectionWin Connection;
|
static BaseConnectionWin Connection;
|
||||||
|
|
||||||
/*static*/ BaseConnection *BaseConnection::Create() {
|
BaseConnection *BaseConnection::Create() {
|
||||||
return &Connection;
|
return &Connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) {
|
void BaseConnection::Destroy(BaseConnection *&c) {
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionWin*>(c);
|
auto self = reinterpret_cast<BaseConnectionWin*>(c);
|
||||||
self->Close();
|
self->Close();
|
||||||
c = nullptr;
|
c = nullptr;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BaseConnection::Open() {
|
bool BaseConnection::Open() {
|
||||||
|
|
||||||
wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" };
|
wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" };
|
||||||
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
||||||
pipeName[pipeDigit] = L'0';
|
pipeName[pipeDigit] = L'0';
|
||||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
self->pipe = ::CreateFileW(
|
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||||
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
|
||||||
if (self->pipe != INVALID_HANDLE_VALUE) {
|
if (self->pipe != INVALID_HANDLE_VALUE) {
|
||||||
self->isOpen = true;
|
self->isOpen = true;
|
||||||
return true;
|
return true;
|
||||||
@@ -57,17 +83,22 @@ bool BaseConnection::Open() {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BaseConnection::Close() {
|
bool BaseConnection::Close() {
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||||
::CloseHandle(self->pipe);
|
::CloseHandle(self->pipe);
|
||||||
self->pipe = INVALID_HANDLE_VALUE;
|
self->pipe = INVALID_HANDLE_VALUE;
|
||||||
self->isOpen = false;
|
self->isOpen = false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool BaseConnection::Write(const void *data, size_t length) {
|
bool BaseConnection::Write(const void *data, size_t length) {
|
||||||
|
|
||||||
if (length == 0) {
|
if (length == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -85,11 +116,13 @@ bool BaseConnection::Write(const void *data, size_t length) {
|
|||||||
}
|
}
|
||||||
const DWORD bytesLength = static_cast<DWORD>(length);
|
const DWORD bytesLength = static_cast<DWORD>(length);
|
||||||
DWORD bytesWritten = 0;
|
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) {
|
bool BaseConnection::Read(void *data, size_t length) {
|
||||||
|
|
||||||
assert(data);
|
assert(data);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return false;
|
return false;
|
||||||
@@ -119,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
|
|||||||
else {
|
else {
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace discord_rpc
|
} // 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_rpc.h"
|
||||||
#include "discord_register.h"
|
#include "discord_register.h"
|
||||||
|
|
||||||
@@ -5,6 +28,7 @@
|
|||||||
#define NOMCX
|
#define NOMCX
|
||||||
#define NOSERVICE
|
#define NOSERVICE
|
||||||
#define NOIME
|
#define NOIME
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <psapi.h>
|
#include <psapi.h>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -46,12 +70,8 @@ static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat,
|
|||||||
#endif
|
#endif
|
||||||
#define RegSetKeyValueW regset
|
#define RegSetKeyValueW regset
|
||||||
|
|
||||||
static LSTATUS regset(HKEY hkey,
|
static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) {
|
||||||
LPCWSTR subkey,
|
|
||||||
LPCWSTR name,
|
|
||||||
DWORD type,
|
|
||||||
const void *data,
|
|
||||||
DWORD len) {
|
|
||||||
HKEY htkey = hkey, hsubkey = nullptr;
|
HKEY htkey = hkey, hsubkey = nullptr;
|
||||||
LSTATUS ret;
|
LSTATUS ret;
|
||||||
if (subkey && subkey[0]) {
|
if (subkey && subkey[0]) {
|
||||||
@@ -64,16 +84,18 @@ static LSTATUS regset(HKEY hkey,
|
|||||||
if (hsubkey && hsubkey != hkey)
|
if (hsubkey && hsubkey != hkey)
|
||||||
RegCloseKey(hsubkey);
|
RegCloseKey(hsubkey);
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
|
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
|
||||||
|
|
||||||
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
|
// 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>://
|
// 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.
|
// 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);
|
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
|
||||||
wchar_t openCommand[1024];
|
wchar_t openCommand[1024]{};
|
||||||
|
|
||||||
if (command && command[0]) {
|
if (command && command[0]) {
|
||||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
|
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);
|
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
wchar_t protocolName[64];
|
wchar_t protocolName[64]{};
|
||||||
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
|
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
|
||||||
wchar_t protocolDescription[128];
|
wchar_t protocolDescription[128]{};
|
||||||
StringCbPrintfW(
|
StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
|
||||||
protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
|
|
||||||
wchar_t urlProtocol = 0;
|
wchar_t urlProtocol = 0;
|
||||||
|
|
||||||
wchar_t keyName[256];
|
wchar_t keyName[256]{};
|
||||||
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
|
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
|
||||||
HKEY key;
|
HKEY key;
|
||||||
auto status =
|
auto status = RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
|
||||||
RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
|
|
||||||
if (status != ERROR_SUCCESS) {
|
if (status != ERROR_SUCCESS) {
|
||||||
fprintf(stderr, "Error creating key\n");
|
fprintf(stderr, "Error creating key\n");
|
||||||
return;
|
return;
|
||||||
@@ -102,8 +122,7 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
|
|||||||
DWORD len;
|
DWORD len;
|
||||||
LSTATUS result;
|
LSTATUS result;
|
||||||
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
|
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
|
||||||
result =
|
result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
|
||||||
RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
|
|
||||||
if (FAILED(result)) {
|
if (FAILED(result)) {
|
||||||
fprintf(stderr, "Error writing description\n");
|
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");
|
fprintf(stderr, "Error writing description\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
result = RegSetKeyValueW(
|
result = RegSetKeyValueW(key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
|
||||||
key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
|
|
||||||
if (FAILED(result)) {
|
if (FAILED(result)) {
|
||||||
fprintf(stderr, "Error writing icon\n");
|
fprintf(stderr, "Error writing icon\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
|
len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
|
||||||
result = RegSetKeyValueW(
|
result = RegSetKeyValueW(key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
|
||||||
key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
|
|
||||||
if (FAILED(result)) {
|
if (FAILED(result)) {
|
||||||
fprintf(stderr, "Error writing command\n");
|
fprintf(stderr, "Error writing command\n");
|
||||||
}
|
}
|
||||||
RegCloseKey(key);
|
RegCloseKey(key);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" void Discord_Register(const char *applicationId, const char *command) {
|
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);
|
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
|
||||||
|
|
||||||
wchar_t openCommand[1024];
|
wchar_t openCommand[1024]{};
|
||||||
const wchar_t *wcommand = nullptr;
|
const wchar_t *wcommand = nullptr;
|
||||||
if (command && command[0]) {
|
if (command && command[0]) {
|
||||||
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
|
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);
|
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"
|
/*
|
||||||
|
* Copyright 2017 Discord, Inc.
|
||||||
#include "backoff.h"
|
*
|
||||||
#include "discord_register.h"
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
#include "msg_queue.h"
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
#include "rpc_connection.h"
|
* the Software without restriction, including without limitation the rights to
|
||||||
#include "serialization.h"
|
* 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 <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <thread>
|
#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 MaxMessageSize { 16 * 1024 };
|
||||||
constexpr size_t MessageQueueSize { 8 };
|
constexpr size_t MessageQueueSize { 8 };
|
||||||
@@ -67,14 +92,12 @@ static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
|||||||
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
||||||
static User connectedUser;
|
static User connectedUser;
|
||||||
|
|
||||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential
|
// 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
|
||||||
// backoff from 0.5 seconds to 1 minute
|
|
||||||
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
||||||
static auto NextConnect = std::chrono::system_clock::now();
|
static auto NextConnect = std::chrono::system_clock::now();
|
||||||
static int Pid { 0 };
|
static int Pid { 0 };
|
||||||
static int Nonce { 1 };
|
static int Nonce { 1 };
|
||||||
|
|
||||||
static void Discord_UpdateConnection(void);
|
|
||||||
class IoThreadHolder {
|
class IoThreadHolder {
|
||||||
private:
|
private:
|
||||||
std::atomic_bool keepRunning { true };
|
std::atomic_bool keepRunning { true };
|
||||||
@@ -108,14 +131,55 @@ class IoThreadHolder {
|
|||||||
|
|
||||||
~IoThreadHolder() { Stop(); }
|
~IoThreadHolder() { Stop(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
static IoThreadHolder *IoThread { nullptr };
|
static IoThreadHolder *IoThread { nullptr };
|
||||||
|
|
||||||
static void UpdateReconnectTime() {
|
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) {
|
if (!Connection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -217,54 +281,18 @@ static void Discord_UpdateConnection(void) {
|
|||||||
SendQueue.CommitSend();
|
SendQueue.CommitSend();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SignalIOActivity() {
|
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
|
||||||
if (IoThread != nullptr) {
|
|
||||||
IoThread->Notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool RegisterForEvent(const char *evtName) {
|
|
||||||
auto qmessage = SendQueue.GetNextAddMessage();
|
|
||||||
if (qmessage) {
|
|
||||||
qmessage->length =
|
|
||||||
JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
|
||||||
SendQueue.CommitAdd();
|
|
||||||
SignalIOActivity();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool DeregisterForEvent(const char *evtName) {
|
|
||||||
auto qmessage = SendQueue.GetNextAddMessage();
|
|
||||||
if (qmessage) {
|
|
||||||
qmessage->length =
|
|
||||||
JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
|
||||||
SendQueue.CommitAdd();
|
|
||||||
SignalIOActivity();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void Discord_Initialize(const char *applicationId,
|
|
||||||
DiscordEventHandlers *handlers,
|
|
||||||
int autoRegister,
|
|
||||||
const char *optionalSteamId) {
|
|
||||||
IoThread = new (std::nothrow) IoThreadHolder();
|
IoThread = new (std::nothrow) IoThreadHolder();
|
||||||
if (IoThread == nullptr) {
|
if (IoThread == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autoRegister) {
|
if (autoRegister) {
|
||||||
if (optionalSteamId && optionalSteamId[0]) {
|
Discord_Register(applicationId, nullptr);
|
||||||
Discord_RegisterSteamGame(applicationId, optionalSteamId);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Discord_Register(applicationId, nullptr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Pid = GetProcessId();
|
Pid = GetProcessId();
|
||||||
@@ -323,9 +351,11 @@ extern "C" void Discord_Initialize(const char *applicationId,
|
|||||||
};
|
};
|
||||||
|
|
||||||
IoThread->Start();
|
IoThread->Start();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" void Discord_Shutdown(void) {
|
extern "C" void Discord_Shutdown() {
|
||||||
|
|
||||||
if (!Connection) {
|
if (!Connection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -341,16 +371,19 @@ extern "C" void Discord_Shutdown(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RpcConnection::Destroy(Connection);
|
RpcConnection::Destroy(Connection);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
|
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
|
||||||
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||||
QueuedPresence.length = JsonWriteRichPresenceObj(
|
QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
|
||||||
QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
|
|
||||||
UpdatePresence.exchange(true);
|
UpdatePresence.exchange(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
SignalIOActivity();
|
SignalIOActivity();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" void Discord_ClearPresence(void) {
|
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) {
|
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 we are not connected, let's not batch up stale messages for later
|
||||||
if (!Connection || !Connection->IsOpen()) {
|
if (!Connection || !Connection->IsOpen()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto qmessage = SendQueue.GetNextAddMessage();
|
auto qmessage = SendQueue.GetNextAddMessage();
|
||||||
if (qmessage) {
|
if (qmessage) {
|
||||||
qmessage->length =
|
qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
|
||||||
JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
|
|
||||||
SendQueue.CommitAdd();
|
SendQueue.CommitAdd();
|
||||||
SignalIOActivity();
|
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
|
// 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
|
// 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.
|
// signals are book-ended by calls to ready and disconnect.
|
||||||
@@ -380,8 +415,8 @@ extern "C" void Discord_RunCallbacks(void) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool wasDisconnected = WasJustDisconnected.exchange(false);
|
const bool wasDisconnected = WasJustDisconnected.exchange(false);
|
||||||
bool isConnected = Connection->IsOpen();
|
const bool isConnected = Connection->IsOpen();
|
||||||
|
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
// if we are connected, disconnect cb first
|
// if we are connected, disconnect cb first
|
||||||
@@ -394,10 +429,7 @@ extern "C" void Discord_RunCallbacks(void) {
|
|||||||
if (WasJustConnected.exchange(false)) {
|
if (WasJustConnected.exchange(false)) {
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||||
if (Handlers.ready) {
|
if (Handlers.ready) {
|
||||||
DiscordUser du { connectedUser.userId,
|
DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||||
connectedUser.username,
|
|
||||||
connectedUser.discriminator,
|
|
||||||
connectedUser.avatar };
|
|
||||||
Handlers.ready(&du);
|
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
|
// 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.
|
// not it should be trivial for the implementer to make a queue themselves.
|
||||||
while (JoinAskQueue.HavePendingSends()) {
|
while (JoinAskQueue.HavePendingSends()) {
|
||||||
auto req = JoinAskQueue.GetNextSendMessage();
|
const auto req = JoinAskQueue.GetNextSendMessage();
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||||
if (Handlers.joinRequest) {
|
if (Handlers.joinRequest) {
|
||||||
@@ -447,9 +479,11 @@ extern "C" void Discord_RunCallbacks(void) {
|
|||||||
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
||||||
|
|
||||||
if (newHandlers) {
|
if (newHandlers) {
|
||||||
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
||||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
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);
|
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||||
Handlers = {};
|
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 {
|
namespace discord_rpc {
|
||||||
|
|
||||||
static const int RpcVersion = 1;
|
static const int RpcVersion = 1;
|
||||||
static RpcConnection Instance;
|
static RpcConnection Instance;
|
||||||
|
|
||||||
/*static*/ RpcConnection *RpcConnection::Create(const char *applicationId) {
|
RpcConnection *RpcConnection::Create(const char *applicationId) {
|
||||||
|
|
||||||
Instance.connection = BaseConnection::Create();
|
Instance.connection = BaseConnection::Create();
|
||||||
StringCopy(Instance.appId, applicationId);
|
StringCopy(Instance.appId, applicationId);
|
||||||
return &Instance;
|
return &Instance;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*static*/ void RpcConnection::Destroy(RpcConnection *&c) {
|
void RpcConnection::Destroy(RpcConnection *&c) {
|
||||||
|
|
||||||
c->Close();
|
c->Close();
|
||||||
BaseConnection::Destroy(c->connection);
|
BaseConnection::Destroy(c->connection);
|
||||||
c = nullptr;
|
c = nullptr;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RpcConnection::Open() {
|
void RpcConnection::Open() {
|
||||||
|
|
||||||
if (state == State::Connected) {
|
if (state == State::Connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -51,17 +79,21 @@ void RpcConnection::Open() {
|
|||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RpcConnection::Close() {
|
void RpcConnection::Close() {
|
||||||
|
|
||||||
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
|
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
|
||||||
onDisconnect(lastErrorCode, lastErrorMessage);
|
onDisconnect(lastErrorCode, lastErrorMessage);
|
||||||
}
|
}
|
||||||
connection->Close();
|
connection->Close();
|
||||||
state = State::Disconnected;
|
state = State::Disconnected;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RpcConnection::Write(const void *data, size_t length) {
|
bool RpcConnection::Write(const void *data, size_t length) {
|
||||||
|
|
||||||
sendFrame.opcode = Opcode::Frame;
|
sendFrame.opcode = Opcode::Frame;
|
||||||
memcpy(sendFrame.message, data, length);
|
memcpy(sendFrame.message, data, length);
|
||||||
sendFrame.length = static_cast<uint32_t>(length);
|
sendFrame.length = static_cast<uint32_t>(length);
|
||||||
@@ -69,14 +101,17 @@ bool RpcConnection::Write(const void *data, size_t length) {
|
|||||||
Close();
|
Close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RpcConnection::Read(JsonDocument &message) {
|
bool RpcConnection::Read(JsonDocument &message) {
|
||||||
|
|
||||||
if (state != State::Connected && state != State::SentHandshake) {
|
if (state != State::Connected && state != State::SentHandshake) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
MessageFrame readFrame;
|
MessageFrame readFrame{};
|
||||||
for (;;) {
|
for (;;) {
|
||||||
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
|
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
|
||||||
if (!didRead) {
|
if (!didRead) {
|
||||||
@@ -127,7 +162,7 @@ bool RpcConnection::Read(JsonDocument &message) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace discord_rpc
|
} // 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"
|
#include "discord_rpc.h"
|
||||||
|
|
||||||
namespace discord_rpc {
|
namespace discord_rpc {
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
void NumberToString(char *dest, T number) {
|
void NumberToString(char *dest, T number) {
|
||||||
|
|
||||||
if (!number) {
|
if (!number) {
|
||||||
*dest++ = '0';
|
*dest++ = '0';
|
||||||
*dest++ = 0;
|
*dest++ = 0;
|
||||||
@@ -26,6 +50,7 @@ void NumberToString(char *dest, T number) {
|
|||||||
*dest++ = temp[place];
|
*dest++ = temp[place];
|
||||||
}
|
}
|
||||||
*dest = 0;
|
*dest = 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// it's ever so slightly faster to not have to strlen the key
|
// it's ever so slightly faster to not have to strlen the key
|
||||||
@@ -62,24 +87,25 @@ struct WriteArray {
|
|||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
|
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
|
||||||
|
|
||||||
if (value && value[0]) {
|
if (value && value[0]) {
|
||||||
w.Key(k, sizeof(T) - 1);
|
w.Key(k, sizeof(T) - 1);
|
||||||
w.String(value);
|
w.String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void JsonWriteNonce(JsonWriter &writer, int nonce) {
|
static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
|
||||||
|
|
||||||
WriteKey(writer, "nonce");
|
WriteKey(writer, "nonce");
|
||||||
char nonceBuffer[32];
|
char nonceBuffer[32];
|
||||||
NumberToString(nonceBuffer, nonce);
|
NumberToString(nonceBuffer, nonce);
|
||||||
writer.String(nonceBuffer);
|
writer.String(nonceBuffer);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t JsonWriteRichPresenceObj(char *dest,
|
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
|
||||||
size_t maxLen,
|
|
||||||
int nonce,
|
|
||||||
int pid,
|
|
||||||
const DiscordRichPresence *presence) {
|
|
||||||
JsonWriter writer(dest, maxLen);
|
JsonWriter writer(dest, maxLen);
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -102,6 +128,9 @@ size_t JsonWriteRichPresenceObj(char *dest,
|
|||||||
if (presence->type >= 0 && presence->type <= 5) {
|
if (presence->type >= 0 && presence->type <= 5) {
|
||||||
WriteKey(writer, "type");
|
WriteKey(writer, "type");
|
||||||
writer.Int(presence->type);
|
writer.Int(presence->type);
|
||||||
|
|
||||||
|
WriteKey(writer, "status_display_type");
|
||||||
|
writer.Int(presence->status_display_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteOptionalString(writer, "name", presence->name);
|
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) {
|
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
|
||||||
|
|
||||||
JsonWriter writer(dest, maxLen);
|
JsonWriter writer(dest, maxLen);
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -179,9 +209,11 @@ size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char
|
|||||||
}
|
}
|
||||||
|
|
||||||
return writer.Size();
|
return writer.Size();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
||||||
|
|
||||||
JsonWriter writer(dest, maxLen);
|
JsonWriter writer(dest, maxLen);
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -197,9 +229,11 @@ size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const cha
|
|||||||
}
|
}
|
||||||
|
|
||||||
return writer.Size();
|
return writer.Size();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
||||||
|
|
||||||
JsonWriter writer(dest, maxLen);
|
JsonWriter writer(dest, maxLen);
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -215,9 +249,11 @@ size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const c
|
|||||||
}
|
}
|
||||||
|
|
||||||
return writer.Size();
|
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);
|
JsonWriter writer(dest, maxLen);
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -243,7 +279,7 @@ size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int rep
|
|||||||
}
|
}
|
||||||
|
|
||||||
return writer.Size();
|
return writer.Size();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace discord_rpc
|
} // 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/document.h>
|
||||||
#include <rapidjson/stringbuffer.h>
|
#include <rapidjson/stringbuffer.h>
|
||||||
#include <rapidjson/writer.h>
|
#include <rapidjson/writer.h>
|
||||||
|
|
||||||
|
struct DiscordRichPresence;
|
||||||
|
|
||||||
namespace discord_rpc {
|
namespace discord_rpc {
|
||||||
|
|
||||||
// if only there was a standard library function for this
|
// 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);
|
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
|
||||||
|
|
||||||
// Commands
|
// Commands
|
||||||
struct DiscordRichPresence;
|
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
|
||||||
size_t JsonWriteRichPresenceObj(char *dest,
|
|
||||||
size_t maxLen,
|
|
||||||
int nonce,
|
|
||||||
int pid,
|
|
||||||
const DiscordRichPresence *presence);
|
|
||||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
||||||
|
|
||||||
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
size_t 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>;
|
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
|
||||||
|
|
||||||
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
|
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
|
||||||
|
|
||||||
if (obj) {
|
if (obj) {
|
||||||
auto member = obj->FindMember(name);
|
auto member = obj->FindMember(name);
|
||||||
if (member != obj->MemberEnd() && member->value.IsObject()) {
|
if (member != obj->MemberEnd() && member->value.IsObject()) {
|
||||||
return &member->value;
|
return &member->value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
|
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
|
||||||
|
|
||||||
if (obj) {
|
if (obj) {
|
||||||
auto member = obj->FindMember(name);
|
auto member = obj->FindMember(name);
|
||||||
if (member != obj->MemberEnd() && member->value.IsInt()) {
|
if (member != obj->MemberEnd() && member->value.IsInt()) {
|
||||||
return member->value.GetInt();
|
return member->value.GetInt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return notFoundDefault;
|
return notFoundDefault;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline const char *GetStrMember(JsonValue *obj,
|
inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
|
||||||
const char *name,
|
|
||||||
const char *notFoundDefault = nullptr) {
|
|
||||||
if (obj) {
|
if (obj) {
|
||||||
auto member = obj->FindMember(name);
|
auto member = obj->FindMember(name);
|
||||||
if (member != obj->MemberEnd() && member->value.IsString()) {
|
if (member != obj->MemberEnd() && member->value.IsString()) {
|
||||||
return member->value.GetString();
|
return member->value.GetString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return notFoundDefault;
|
return notFoundDefault;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace discord_rpc
|
} // 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()
|
endif()
|
||||||
|
|
||||||
if(WIN32)
|
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()
|
endif()
|
||||||
|
|
||||||
if(APPLE OR WIN32)
|
if(APPLE OR WIN32)
|
||||||
@@ -783,9 +792,7 @@ set(SOURCES
|
|||||||
src/scrobbler/scrobblercache.cpp
|
src/scrobbler/scrobblercache.cpp
|
||||||
src/scrobbler/scrobblercacheitem.cpp
|
src/scrobbler/scrobblercacheitem.cpp
|
||||||
src/scrobbler/scrobblemetadata.cpp
|
src/scrobbler/scrobblemetadata.cpp
|
||||||
src/scrobbler/scrobblingapi20.cpp
|
|
||||||
src/scrobbler/lastfmscrobbler.cpp
|
src/scrobbler/lastfmscrobbler.cpp
|
||||||
src/scrobbler/librefmscrobbler.cpp
|
|
||||||
src/scrobbler/listenbrainzscrobbler.cpp
|
src/scrobbler/listenbrainzscrobbler.cpp
|
||||||
src/scrobbler/lastfmimport.cpp
|
src/scrobbler/lastfmimport.cpp
|
||||||
|
|
||||||
@@ -1076,9 +1083,7 @@ set(HEADERS
|
|||||||
src/scrobbler/scrobblersettingsservice.h
|
src/scrobbler/scrobblersettingsservice.h
|
||||||
src/scrobbler/scrobblerservice.h
|
src/scrobbler/scrobblerservice.h
|
||||||
src/scrobbler/scrobblercache.h
|
src/scrobbler/scrobblercache.h
|
||||||
src/scrobbler/scrobblingapi20.h
|
|
||||||
src/scrobbler/lastfmscrobbler.h
|
src/scrobbler/lastfmscrobbler.h
|
||||||
src/scrobbler/librefmscrobbler.h
|
|
||||||
src/scrobbler/listenbrainzscrobbler.h
|
src/scrobbler/listenbrainzscrobbler.h
|
||||||
src/scrobbler/lastfmimport.h
|
src/scrobbler/lastfmimport.h
|
||||||
|
|
||||||
@@ -1494,7 +1499,7 @@ endif()
|
|||||||
|
|
||||||
if(HAVE_DISCORD_RPC)
|
if(HAVE_DISCORD_RPC)
|
||||||
add_subdirectory(3rdparty/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()
|
endif()
|
||||||
|
|
||||||
if(HAVE_TRANSLATIONS)
|
if(HAVE_TRANSLATIONS)
|
||||||
@@ -1554,9 +1559,10 @@ target_link_libraries(strawberry_lib PUBLIC
|
|||||||
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
|
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
|
||||||
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
|
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
|
||||||
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
|
$<$<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>
|
$<$<BOOL:${MSVC}>:WindowsApp>
|
||||||
KDAB::kdsingleapplication
|
KDAB::kdsingleapplication
|
||||||
|
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
|
||||||
)
|
)
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
@@ -1575,10 +1581,6 @@ if(APPLE)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(HAVE_DISCORD_RPC)
|
|
||||||
target_link_libraries(strawberry_lib PRIVATE discord-rpc)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(strawberry PUBLIC strawberry_lib)
|
target_link_libraries(strawberry PUBLIC strawberry_lib)
|
||||||
|
|
||||||
if(NOT APPLE)
|
if(NOT APPLE)
|
||||||
|
|||||||
91
Changelog
91
Changelog
@@ -2,6 +2,97 @@ Strawberry Music Player
|
|||||||
=======================
|
=======================
|
||||||
ChangeLog
|
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):
|
Version 1.2.8 (2025.04.05):
|
||||||
|
|
||||||
Bugfixes:
|
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://github.com/sponsors/jonaski)
|
||||||
[](https://patreon.com/jonaskvinge)
|
[](https://patreon.com/jonaskvinge)
|
||||||
[](https://paypal.me/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/
|
## :globe_with_meridians: Resources
|
||||||
* 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/
|
|
||||||
|
|
||||||
### :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.
|
Before creating a new GitHub issue:
|
||||||
There are currently 4 options for sponsoring:
|
|
||||||
|
|
||||||
|
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)
|
1. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||||
2. [GitHub](https://github.com/sponsors/jonaski)
|
2. [GitHub](https://github.com/sponsors/jonaski)
|
||||||
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
||||||
4. [PayPal](https://paypal.me/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
|
## :white_check_mark: Features
|
||||||
* 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
|
|
||||||
|
|
||||||
|
- 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/)
|
## :gear: Requirements
|
||||||
* 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)
|
|
||||||
|
|
||||||
Optional dependencies:
|
To build Strawberry from source, you’ll need:
|
||||||
|
|
||||||
* Song fingerprinting and MusicBrainz tagging: [Chromaprint](https://acoustid.org/chromaprint)
|
**Dependencies:**
|
||||||
* Moodbar: [fftw3](http://www.fftw.org/)
|
- [CMake ≥= 3.13](https://cmake.org/)
|
||||||
* PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/?)
|
- C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/), or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
|
||||||
* Audio CD: [libcdio](https://www.gnu.org/software/libcdio/)
|
- [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
||||||
* MTP devices: [libmtp](http://libmtp.sourceforge.net/)
|
- [Boost](https://www.boost.org/)
|
||||||
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
|
- [GLib](https://developer.gnome.org/glib/)
|
||||||
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
|
- [Qt ≥= 6.4](https://www.qt.io/) (Core, Concurrent, Gui, Widgets, Network, SQL, D-Bus)
|
||||||
* Discord rich presence [RapidJSON](https://rapidjson.org/)
|
- [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
|
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
|
||||||
|
|
||||||
### Build and install:
|
**Build and install:**
|
||||||
|
|
||||||
cd strawberry
|
cd strawberry
|
||||||
cmake -S . -B build
|
cmake -S . -B build
|
||||||
cmake --build build --parallel $(nproc)
|
cmake --build build --parallel $(nproc)
|
||||||
sudo cmake --install build
|
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)
|
[](https://repology.org/metapackage/strawberry/versions)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||||
set(STRAWBERRY_VERSION_MINOR 2)
|
set(STRAWBERRY_VERSION_MINOR 2)
|
||||||
set(STRAWBERRY_VERSION_PATCH 8)
|
set(STRAWBERRY_VERSION_PATCH 14)
|
||||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||||
|
|
||||||
set(INCLUDE_GIT_REVISION OFF)
|
set(INCLUDE_GIT_REVISION OFF)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<file>schema/schema-18.sql</file>
|
<file>schema/schema-18.sql</file>
|
||||||
<file>schema/schema-19.sql</file>
|
<file>schema/schema-19.sql</file>
|
||||||
<file>schema/schema-20.sql</file>
|
<file>schema/schema-20.sql</file>
|
||||||
|
<file>schema/schema-21.sql</file>
|
||||||
<file>schema/device-schema.sql</file>
|
<file>schema/device-schema.sql</file>
|
||||||
<file>style/strawberry.css</file>
|
<file>style/strawberry.css</file>
|
||||||
<file>style/smartplaylistsearchterm.css</file>
|
<file>style/smartplaylistsearchterm.css</file>
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ CREATE TABLE device_%deviceid_subdirectories (
|
|||||||
CREATE TABLE device_%deviceid_songs (
|
CREATE TABLE device_%deviceid_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -22,7 +26,9 @@ CREATE TABLE device_%deviceid_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -86,7 +92,11 @@ CREATE TABLE device_%deviceid_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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);
|
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;
|
DELETE FROM schema_version;
|
||||||
|
|
||||||
INSERT INTO schema_version (version) VALUES (20);
|
INSERT INTO schema_version (version) VALUES (21);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS directories (
|
CREATE TABLE IF NOT EXISTS directories (
|
||||||
path TEXT NOT NULL,
|
path TEXT NOT NULL,
|
||||||
@@ -20,9 +20,13 @@ CREATE TABLE IF NOT EXISTS subdirectories (
|
|||||||
CREATE TABLE IF NOT EXISTS songs (
|
CREATE TABLE IF NOT EXISTS songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -30,7 +34,9 @@ CREATE TABLE IF NOT EXISTS songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -94,16 +100,24 @@ CREATE TABLE IF NOT EXISTS songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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 (
|
CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -111,7 +125,9 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -175,16 +191,24 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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 (
|
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year 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,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -256,16 +282,24 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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 (
|
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year 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,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -337,16 +373,24 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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 (
|
CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -354,7 +398,9 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -418,16 +464,24 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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 (
|
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year 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,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -499,16 +555,24 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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 (
|
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year 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,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -580,16 +646,24 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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 (
|
CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -597,7 +671,9 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -661,16 +737,24 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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 (
|
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year 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,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -742,16 +828,24 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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 (
|
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year 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,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -823,16 +919,24 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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 (
|
CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -840,7 +944,9 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -904,7 +1010,11 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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,
|
playlist_url TEXT,
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER,
|
track INTEGER,
|
||||||
disc INTEGER,
|
disc INTEGER,
|
||||||
year INTEGER,
|
year INTEGER,
|
||||||
@@ -941,7 +1055,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER DEFAULT 0,
|
compilation INTEGER DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -1005,7 +1121,11 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
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_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_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_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_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;
|
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
|
- Edit tags on audio files
|
||||||
- Automatically retrieve tags from MusicBrainz
|
- Automatically retrieve tags from MusicBrainz
|
||||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
- 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 analyzer
|
||||||
- Audio equalizer
|
- Audio equalizer
|
||||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
- 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
|
- Streaming support for Subsonic-compatible servers
|
||||||
- Unofficial streaming support for Tidal and Qobuz
|
- 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>Edit tags on audio files</li>
|
||||||
<li>Automatically retrieve tags from MusicBrainz</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>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>Audio analyzer and equalizer</li>
|
||||||
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</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>Streaming support for Subsonic-compatible servers</li>
|
||||||
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -51,6 +51,12 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||||
<releases>
|
<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.8" date="2025-04-05"/>
|
||||||
<release version="1.2.7" date="2025-01-31"/>
|
<release version="1.2.7" date="2025-01-31"/>
|
||||||
<release version="1.2.6" date="2025-01-17"/>
|
<release version="1.2.6" date="2025-01-17"/>
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ TryExec=strawberry
|
|||||||
Icon=strawberry
|
Icon=strawberry
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=AudioVideo;Player;Qt;Audio;
|
Categories=AudioVideo;Player;Qt;Audio;
|
||||||
Keywords=Audio;Player;
|
Keywords=Audio;Player;Clementine;
|
||||||
StartupNotify=false
|
|
||||||
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;
|
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
|
StartupWMClass=strawberry
|
||||||
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
|
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
|
.br
|
||||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||||
.br
|
.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
|
.br
|
||||||
- Support for multiple backends
|
- Support for multiple backends
|
||||||
.br
|
.br
|
||||||
@@ -39,7 +39,7 @@ Features:
|
|||||||
.br
|
.br
|
||||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
.br
|
.br
|
||||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
- Scrobbler with support for Last.fm and ListenBrainz
|
||||||
.br
|
.br
|
||||||
- Streaming support for Subsonic-compatible servers
|
- Streaming support for Subsonic-compatible servers
|
||||||
.br
|
.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
|
- Edit tags on audio files
|
||||||
- Automatically retrieve tags from MusicBrainz
|
- Automatically retrieve tags from MusicBrainz
|
||||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
- 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
|
- Support for multiple backends
|
||||||
- Audio analyzer
|
- Audio analyzer
|
||||||
- Audio equalizer
|
- 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
|
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
- Streaming support for Subsonic-compatible servers
|
- Streaming support for Subsonic-compatible servers
|
||||||
- Unofficial streaming support for Tidal and Qobuz
|
- 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
|
%debug_package
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
|
|||||||
103
dist/windows/strawberry.nsi.in
vendored
103
dist/windows/strawberry.nsi.in
vendored
@@ -21,6 +21,10 @@
|
|||||||
!define arch_x64
|
!define arch_x64
|
||||||
!else if "@ARCH@" == "x86_64-w64-mingw32.shared"
|
!else if "@ARCH@" == "x86_64-w64-mingw32.shared"
|
||||||
!define arch_x64
|
!define arch_x64
|
||||||
|
!else if "@ARCH@" == "arm64"
|
||||||
|
!define arch_arm64
|
||||||
|
!else
|
||||||
|
!error "Missing ARCH"
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!ifdef arch_x86
|
!ifdef arch_x86
|
||||||
@@ -31,6 +35,10 @@
|
|||||||
!define arch "x64"
|
!define arch "x64"
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
|
!ifdef arch_arm64
|
||||||
|
!define arch "arm64"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
|
||||||
!if "@CMAKE_BUILD_TYPE@" == "Release"
|
!if "@CMAKE_BUILD_TYPE@" == "Release"
|
||||||
!define release
|
!define release
|
||||||
@@ -38,6 +46,8 @@
|
|||||||
!define release
|
!define release
|
||||||
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
|
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
|
||||||
!define debug
|
!define debug
|
||||||
|
!else
|
||||||
|
!error "Missing CMAKE_BUILD_TYPE"
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!ifdef release
|
!ifdef release
|
||||||
@@ -70,7 +80,7 @@
|
|||||||
!ifdef arch_x86
|
!ifdef arch_x86
|
||||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug"
|
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug"
|
||||||
!endif
|
!endif
|
||||||
!ifdef arch_x64
|
!ifdef arch_x64 || arch_arm64
|
||||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug"
|
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug"
|
||||||
!endif
|
!endif
|
||||||
!else
|
!else
|
||||||
@@ -80,7 +90,7 @@
|
|||||||
!ifdef arch_x86
|
!ifdef arch_x86
|
||||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player"
|
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player"
|
||||||
!endif
|
!endif
|
||||||
!ifdef arch_x64
|
!ifdef arch_x64 || arch_arm64
|
||||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player"
|
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player"
|
||||||
!endif
|
!endif
|
||||||
!endif
|
!endif
|
||||||
@@ -214,7 +224,7 @@ Function InstallMSVCRuntime
|
|||||||
; ${If} $R0 == ""
|
; ${If} $R0 == ""
|
||||||
SetDetailsView hide
|
SetDetailsView hide
|
||||||
; inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
|
; 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}"
|
Delete "$TEMP\${vc_redist_file}"
|
||||||
SetDetailsView show
|
SetDetailsView show
|
||||||
; ${EndIf}
|
; ${EndIf}
|
||||||
@@ -324,7 +334,7 @@ Section "Strawberry" Strawberry
|
|||||||
File "libqtsparkle-qt6.dll"
|
File "libqtsparkle-qt6.dll"
|
||||||
File "libsoup-3.0-0.dll"
|
File "libsoup-3.0-0.dll"
|
||||||
File "libspeex-1.dll"
|
File "libspeex-1.dll"
|
||||||
File "libsqlite3.dll"
|
File "libsqlite3-0.dll"
|
||||||
File "libssp-0.dll"
|
File "libssp-0.dll"
|
||||||
File "libstdc++-6.dll"
|
File "libstdc++-6.dll"
|
||||||
File "libtag.dll"
|
File "libtag.dll"
|
||||||
@@ -367,6 +377,10 @@ Section "Strawberry" Strawberry
|
|||||||
File "libcrypto-3-x64.dll"
|
File "libcrypto-3-x64.dll"
|
||||||
File "libssl-3-x64.dll"
|
File "libssl-3-x64.dll"
|
||||||
!endif
|
!endif
|
||||||
|
!ifdef arch_arm64
|
||||||
|
File "libcrypto-3-arm64.dll"
|
||||||
|
File "libssl-3-arm64.dll"
|
||||||
|
!endif
|
||||||
|
|
||||||
File "FLAC.dll"
|
File "FLAC.dll"
|
||||||
File "brotlicommon.dll"
|
File "brotlicommon.dll"
|
||||||
@@ -381,7 +395,6 @@ Section "Strawberry" Strawberry
|
|||||||
File "glib-2.0-0.dll"
|
File "glib-2.0-0.dll"
|
||||||
File "gme.dll"
|
File "gme.dll"
|
||||||
File "gmodule-2.0-0.dll"
|
File "gmodule-2.0-0.dll"
|
||||||
File "gnutls.dll"
|
|
||||||
File "gobject-2.0-0.dll"
|
File "gobject-2.0-0.dll"
|
||||||
File "gstadaptivedemux-1.0-0.dll"
|
File "gstadaptivedemux-1.0-0.dll"
|
||||||
File "gstapp-1.0-0.dll"
|
File "gstapp-1.0-0.dll"
|
||||||
@@ -402,14 +415,11 @@ Section "Strawberry" Strawberry
|
|||||||
File "gsttag-1.0-0.dll"
|
File "gsttag-1.0-0.dll"
|
||||||
File "gsturidownloader-1.0-0.dll"
|
File "gsturidownloader-1.0-0.dll"
|
||||||
File "gstvideo-1.0-0.dll"
|
File "gstvideo-1.0-0.dll"
|
||||||
File "gstwinrt-1.0-0.dll"
|
|
||||||
File "harfbuzz.dll"
|
File "harfbuzz.dll"
|
||||||
File "intl-8.dll"
|
File "intl-8.dll"
|
||||||
File "jpeg62.dll"
|
File "jpeg62.dll"
|
||||||
File "kdsingleapplication-qt6.dll"
|
File "kdsingleapplication-qt6.dll"
|
||||||
File "libbs2b.dll"
|
File "libbs2b.dll"
|
||||||
File "libfaac_dll.dll"
|
|
||||||
File "liblzma.dll"
|
|
||||||
File "libmp3lame.dll"
|
File "libmp3lame.dll"
|
||||||
File "libopenmpt.dll"
|
File "libopenmpt.dll"
|
||||||
File "mpcdec.dll"
|
File "mpcdec.dll"
|
||||||
@@ -427,6 +437,11 @@ Section "Strawberry" Strawberry
|
|||||||
File "vorbisfile.dll"
|
File "vorbisfile.dll"
|
||||||
File "wavpackdll.dll"
|
File "wavpackdll.dll"
|
||||||
|
|
||||||
|
!ifndef arch_arm64
|
||||||
|
File "gnutls.dll"
|
||||||
|
File "libfaac_dll.dll"
|
||||||
|
!endif
|
||||||
|
|
||||||
!ifdef release
|
!ifdef release
|
||||||
File "freetype.dll"
|
File "freetype.dll"
|
||||||
File "libiconv.dll"
|
File "libiconv.dll"
|
||||||
@@ -434,8 +449,10 @@ Section "Strawberry" Strawberry
|
|||||||
File "libspeex.dll"
|
File "libspeex.dll"
|
||||||
File "pcre2-8.dll"
|
File "pcre2-8.dll"
|
||||||
File "pcre2-16.dll"
|
File "pcre2-16.dll"
|
||||||
|
File "zlib1.dll"
|
||||||
|
!ifndef arch_arm64
|
||||||
File "twolame.dll"
|
File "twolame.dll"
|
||||||
File "zlib.dll"
|
!endif
|
||||||
!endif
|
!endif
|
||||||
!ifdef debug
|
!ifdef debug
|
||||||
File "freetyped.dll"
|
File "freetyped.dll"
|
||||||
@@ -444,8 +461,10 @@ Section "Strawberry" Strawberry
|
|||||||
File "libspeexd.dll"
|
File "libspeexd.dll"
|
||||||
File "pcre2-8d.dll"
|
File "pcre2-8d.dll"
|
||||||
File "pcre2-16d.dll"
|
File "pcre2-16d.dll"
|
||||||
|
File "zlibd1.dll"
|
||||||
|
!ifndef arch_arm64
|
||||||
File "twolamed.dll"
|
File "twolamed.dll"
|
||||||
File "zlibd.dll"
|
!endif
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
|
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
|
||||||
@@ -459,7 +478,11 @@ Section "Strawberry" Strawberry
|
|||||||
; Common files
|
; Common files
|
||||||
|
|
||||||
File "icudt77.dll"
|
File "icudt77.dll"
|
||||||
|
!ifdef msvc && arch_arm64
|
||||||
|
File "fftw3.dll"
|
||||||
|
!else
|
||||||
File "libfftw3-3.dll"
|
File "libfftw3-3.dll"
|
||||||
|
!endif
|
||||||
!ifdef msvc && debug
|
!ifdef msvc && debug
|
||||||
File "icuin77d.dll"
|
File "icuin77d.dll"
|
||||||
File "icuuc77d.dll"
|
File "icuuc77d.dll"
|
||||||
@@ -526,11 +549,13 @@ Section "GIO modules" gio-modules
|
|||||||
SetOutPath "$INSTDIR\gio-modules"
|
SetOutPath "$INSTDIR\gio-modules"
|
||||||
!ifdef mingw
|
!ifdef mingw
|
||||||
File "/oname=libgiognutls.dll" "gio-modules\libgiognutls.dll"
|
File "/oname=libgiognutls.dll" "gio-modules\libgiognutls.dll"
|
||||||
File "/oname=libgioopenssl.dll" "gio-modules\libgioopenssl.dll"
|
|
||||||
!endif
|
!endif
|
||||||
!ifdef msvc
|
!ifdef msvc
|
||||||
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
|
!ifdef arch_arm64
|
||||||
File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll"
|
File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll"
|
||||||
|
!else
|
||||||
|
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
|
||||||
|
!endif
|
||||||
!endif
|
!endif
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
@@ -647,6 +672,7 @@ Section "Gstreamer plugins" gstreamer-plugins
|
|||||||
File "/oname=libgstvolume.dll" "gstreamer-plugins\libgstvolume.dll"
|
File "/oname=libgstvolume.dll" "gstreamer-plugins\libgstvolume.dll"
|
||||||
File "/oname=libgstvorbis.dll" "gstreamer-plugins\libgstvorbis.dll"
|
File "/oname=libgstvorbis.dll" "gstreamer-plugins\libgstvorbis.dll"
|
||||||
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.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=libgstwaveform.dll" "gstreamer-plugins\libgstwaveform.dll"
|
||||||
File "/oname=libgstwavenc.dll" "gstreamer-plugins\libgstwavenc.dll"
|
File "/oname=libgstwavenc.dll" "gstreamer-plugins\libgstwavenc.dll"
|
||||||
File "/oname=libgstwavpack.dll" "gstreamer-plugins\libgstwavpack.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=gstdirectsound.dll" "gstreamer-plugins\gstdirectsound.dll"
|
||||||
File "/oname=gstdsd.dll" "gstreamer-plugins\gstdsd.dll"
|
File "/oname=gstdsd.dll" "gstreamer-plugins\gstdsd.dll"
|
||||||
File "/oname=gstequalizer.dll" "gstreamer-plugins\gstequalizer.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=gstfaad.dll" "gstreamer-plugins\gstfaad.dll"
|
||||||
File "/oname=gstfdkaac.dll" "gstreamer-plugins\gstfdkaac.dll"
|
File "/oname=gstfdkaac.dll" "gstreamer-plugins\gstfdkaac.dll"
|
||||||
File "/oname=gstflac.dll" "gstreamer-plugins\gstflac.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=gstspeex.dll" "gstreamer-plugins\gstspeex.dll"
|
||||||
File "/oname=gsttaglib.dll" "gstreamer-plugins\gsttaglib.dll"
|
File "/oname=gsttaglib.dll" "gstreamer-plugins\gsttaglib.dll"
|
||||||
File "/oname=gsttcp.dll" "gstreamer-plugins\gsttcp.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=gsttypefindfunctions.dll" "gstreamer-plugins\gsttypefindfunctions.dll"
|
||||||
File "/oname=gstudp.dll" "gstreamer-plugins\gstudp.dll"
|
File "/oname=gstudp.dll" "gstreamer-plugins\gstudp.dll"
|
||||||
File "/oname=gstvolume.dll" "gstreamer-plugins\gstvolume.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=gstwavpack.dll" "gstreamer-plugins\gstwavpack.dll"
|
||||||
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
|
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
|
||||||
File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.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
|
!ifdef arch_x64
|
||||||
;File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
|
File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
|
||||||
!endif
|
!endif
|
||||||
!endif ; MSVC
|
!endif ; MSVC
|
||||||
|
|
||||||
@@ -849,7 +877,7 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\libqtsparkle-qt6.dll"
|
Delete "$INSTDIR\libqtsparkle-qt6.dll"
|
||||||
Delete "$INSTDIR\libsoup-3.0-0.dll"
|
Delete "$INSTDIR\libsoup-3.0-0.dll"
|
||||||
Delete "$INSTDIR\libspeex-1.dll"
|
Delete "$INSTDIR\libspeex-1.dll"
|
||||||
Delete "$INSTDIR\libsqlite3.dll"
|
Delete "$INSTDIR\libsqlite3-0.dll"
|
||||||
Delete "$INSTDIR\libssp-0.dll"
|
Delete "$INSTDIR\libssp-0.dll"
|
||||||
Delete "$INSTDIR\libstdc++-6.dll"
|
Delete "$INSTDIR\libstdc++-6.dll"
|
||||||
Delete "$INSTDIR\libtag.dll"
|
Delete "$INSTDIR\libtag.dll"
|
||||||
@@ -892,6 +920,10 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\libcrypto-3-x64.dll"
|
Delete "$INSTDIR\libcrypto-3-x64.dll"
|
||||||
Delete "$INSTDIR\libssl-3-x64.dll"
|
Delete "$INSTDIR\libssl-3-x64.dll"
|
||||||
!endif
|
!endif
|
||||||
|
!ifdef arch_arm64
|
||||||
|
Delete "$INSTDIR\libcrypto-3-arm64.dll"
|
||||||
|
Delete "$INSTDIR\libssl-3-arm64.dll"
|
||||||
|
!endif
|
||||||
|
|
||||||
Delete "$INSTDIR\FLAC.dll"
|
Delete "$INSTDIR\FLAC.dll"
|
||||||
Delete "$INSTDIR\brotlicommon.dll"
|
Delete "$INSTDIR\brotlicommon.dll"
|
||||||
@@ -906,7 +938,6 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\glib-2.0-0.dll"
|
Delete "$INSTDIR\glib-2.0-0.dll"
|
||||||
Delete "$INSTDIR\gme.dll"
|
Delete "$INSTDIR\gme.dll"
|
||||||
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
||||||
Delete "$INSTDIR\gnutls.dll"
|
|
||||||
Delete "$INSTDIR\gobject-2.0-0.dll"
|
Delete "$INSTDIR\gobject-2.0-0.dll"
|
||||||
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
|
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
|
||||||
Delete "$INSTDIR\gstapp-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\gsttag-1.0-0.dll"
|
||||||
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
|
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
|
||||||
Delete "$INSTDIR\gstvideo-1.0-0.dll"
|
Delete "$INSTDIR\gstvideo-1.0-0.dll"
|
||||||
Delete "$INSTDIR\gstwinrt-1.0-0.dll"
|
|
||||||
Delete "$INSTDIR\harfbuzz.dll"
|
Delete "$INSTDIR\harfbuzz.dll"
|
||||||
Delete "$INSTDIR\intl-8.dll"
|
Delete "$INSTDIR\intl-8.dll"
|
||||||
Delete "$INSTDIR\jpeg62.dll"
|
Delete "$INSTDIR\jpeg62.dll"
|
||||||
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
|
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
|
||||||
Delete "$INSTDIR\libbs2b.dll"
|
Delete "$INSTDIR\libbs2b.dll"
|
||||||
Delete "$INSTDIR\libfaac_dll.dll"
|
|
||||||
Delete "$INSTDIR\liblzma.dll"
|
|
||||||
Delete "$INSTDIR\libmp3lame.dll"
|
Delete "$INSTDIR\libmp3lame.dll"
|
||||||
Delete "$INSTDIR\libopenmpt.dll"
|
Delete "$INSTDIR\libopenmpt.dll"
|
||||||
Delete "$INSTDIR\mpcdec.dll"
|
Delete "$INSTDIR\mpcdec.dll"
|
||||||
@@ -952,6 +980,11 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\vorbisfile.dll"
|
Delete "$INSTDIR\vorbisfile.dll"
|
||||||
Delete "$INSTDIR\wavpackdll.dll"
|
Delete "$INSTDIR\wavpackdll.dll"
|
||||||
|
|
||||||
|
!ifndef arch_arm64
|
||||||
|
Delete "$INSTDIR\gnutls.dll"
|
||||||
|
Delete "$INSTDIR\libfaac_dll.dll"
|
||||||
|
!endif
|
||||||
|
|
||||||
!ifdef release
|
!ifdef release
|
||||||
Delete "$INSTDIR\freetype.dll"
|
Delete "$INSTDIR\freetype.dll"
|
||||||
Delete "$INSTDIR\libiconv.dll"
|
Delete "$INSTDIR\libiconv.dll"
|
||||||
@@ -959,8 +992,10 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\libspeex.dll"
|
Delete "$INSTDIR\libspeex.dll"
|
||||||
Delete "$INSTDIR\pcre2-8.dll"
|
Delete "$INSTDIR\pcre2-8.dll"
|
||||||
Delete "$INSTDIR\pcre2-16.dll"
|
Delete "$INSTDIR\pcre2-16.dll"
|
||||||
|
Delete "$INSTDIR\zlib1.dll"
|
||||||
|
!ifndef arch_arm64
|
||||||
Delete "$INSTDIR\twolame.dll"
|
Delete "$INSTDIR\twolame.dll"
|
||||||
Delete "$INSTDIR\zlib.dll"
|
!endif
|
||||||
!endif
|
!endif
|
||||||
!ifdef debug
|
!ifdef debug
|
||||||
Delete "$INSTDIR\freetyped.dll"
|
Delete "$INSTDIR\freetyped.dll"
|
||||||
@@ -969,8 +1004,10 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\libspeexd.dll"
|
Delete "$INSTDIR\libspeexd.dll"
|
||||||
Delete "$INSTDIR\pcre2-8d.dll"
|
Delete "$INSTDIR\pcre2-8d.dll"
|
||||||
Delete "$INSTDIR\pcre2-16d.dll"
|
Delete "$INSTDIR\pcre2-16d.dll"
|
||||||
|
Delete "$INSTDIR\zlibd1.dll"
|
||||||
|
!ifndef arch_arm64
|
||||||
Delete "$INSTDIR\twolamed.dll"
|
Delete "$INSTDIR\twolamed.dll"
|
||||||
Delete "$INSTDIR\zlibd.dll"
|
!endif
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!ifdef arch_x86
|
!ifdef arch_x86
|
||||||
@@ -983,7 +1020,11 @@ Section "Uninstall"
|
|||||||
; Common files
|
; Common files
|
||||||
|
|
||||||
Delete "$INSTDIR\icudt77.dll"
|
Delete "$INSTDIR\icudt77.dll"
|
||||||
|
!ifdef msvc && arch_arm64
|
||||||
|
Delete "$INSTDIR\fftw3.dll"
|
||||||
|
!else
|
||||||
Delete "$INSTDIR\libfftw3-3.dll"
|
Delete "$INSTDIR\libfftw3-3.dll"
|
||||||
|
!endif
|
||||||
!ifdef msvc && debug
|
!ifdef msvc && debug
|
||||||
Delete "$INSTDIR\icuin77d.dll"
|
Delete "$INSTDIR\icuin77d.dll"
|
||||||
Delete "$INSTDIR\icuuc77d.dll"
|
Delete "$INSTDIR\icuuc77d.dll"
|
||||||
@@ -1016,11 +1057,13 @@ Section "Uninstall"
|
|||||||
|
|
||||||
!ifdef mingw
|
!ifdef mingw
|
||||||
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
|
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
|
||||||
Delete "$INSTDIR\gio-modules\libgioopenssl.dll"
|
|
||||||
!endif
|
!endif
|
||||||
!ifdef msvc
|
!ifdef msvc
|
||||||
Delete "$INSTDIR\gio-modules\giognutls.dll"
|
!ifdef arch_arm64
|
||||||
Delete "$INSTDIR\gio-modules\gioopenssl.dll"
|
Delete "$INSTDIR\gio-modules\gioopenssl.dll"
|
||||||
|
!else
|
||||||
|
Delete "$INSTDIR\gio-modules\giognutls.dll"
|
||||||
|
!endif
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!ifdef msvc && debug
|
!ifdef msvc && debug
|
||||||
@@ -1104,6 +1147,7 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\gstreamer-plugins\libgstvolume.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstvolume.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\libgstvorbis.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstvorbis.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
|
||||||
|
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi2.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\libgstwaveform.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstwaveform.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavenc.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstwavenc.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavpack.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstwavpack.dll"
|
||||||
@@ -1133,7 +1177,6 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\gstreamer-plugins\gstdirectsound.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstdirectsound.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstdsd.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstdsd.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstequalizer.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstequalizer.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
|
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstfaad.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstfaad.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstfdkaac.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstfdkaac.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstflac.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstflac.dll"
|
||||||
@@ -1166,7 +1209,6 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\gstreamer-plugins\gstspeex.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstspeex.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gsttaglib.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gsttaglib.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gsttcp.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gsttcp.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
|
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gsttypefindfunctions.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gsttypefindfunctions.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstudp.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstudp.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstvolume.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstvolume.dll"
|
||||||
@@ -1178,9 +1220,14 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
|
||||||
!ifdef arch_x64
|
!ifndef arch_arm64
|
||||||
;Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
|
||||||
|
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
|
||||||
!endif
|
!endif
|
||||||
|
!ifdef arch_x64
|
||||||
|
Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
|
||||||
|
!endif
|
||||||
|
|
||||||
!endif ; msvc
|
!endif ; msvc
|
||||||
|
|
||||||
Delete "$INSTDIR\Uninstall.exe"
|
Delete "$INSTDIR\Uninstall.exe"
|
||||||
|
|||||||
@@ -623,6 +623,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
|||||||
QMutexLocker l(db_->Mutex());
|
QMutexLocker l(db_->Mutex());
|
||||||
QSqlDatabase db(db_->Connect());
|
QSqlDatabase db(db_->Connect());
|
||||||
|
|
||||||
|
CollectionTask task(task_manager_, tr("Updating %1 database.").arg(Song::TextForSource(source_)));
|
||||||
ScopedTransaction transaction(&db);
|
ScopedTransaction transaction(&db);
|
||||||
|
|
||||||
SongList added_songs;
|
SongList added_songs;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <QUrl>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
@@ -295,19 +296,21 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
|
|||||||
if (version == 1) {
|
if (version == 1) {
|
||||||
QStringList saved = s.childKeys();
|
QStringList saved = s.childKeys();
|
||||||
for (int i = 0; i < saved.size(); ++i) {
|
for (int i = 0; i < saved.size(); ++i) {
|
||||||
if (saved.at(i) == "version"_L1) continue;
|
const QString &name = saved.at(i);
|
||||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
if (name == "version"_L1) continue;
|
||||||
|
QByteArray bytes = s.value(name).toByteArray();
|
||||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||||
CollectionModel::Grouping g;
|
CollectionModel::Grouping g;
|
||||||
ds >> g;
|
ds >> g;
|
||||||
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
|
ret->addAction(CreateGroupByAction(QUrl::fromPercentEncoding(name.toUtf8()), parent, g));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
QStringList saved = s.childKeys();
|
QStringList saved = s.childKeys();
|
||||||
for (int i = 0; i < saved.size(); ++i) {
|
for (int i = 0; i < saved.size(); ++i) {
|
||||||
if (saved.at(i) == "version"_L1) continue;
|
const QString &name = saved.at(i);
|
||||||
s.remove(saved.at(i));
|
if (name == "version"_L1) continue;
|
||||||
|
s.remove(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
@@ -339,7 +342,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
|||||||
|
|
||||||
if (!model_) return;
|
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;
|
if (name.isEmpty()) return;
|
||||||
|
|
||||||
qLog(Debug) << "Saving current grouping to" << name;
|
qLog(Debug) << "Saving current grouping to" << name;
|
||||||
@@ -355,7 +358,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
|||||||
QDataStream datastream(&buffer, QIODevice::WriteOnly);
|
QDataStream datastream(&buffer, QIODevice::WriteOnly);
|
||||||
datastream << model_->GetGroupBy();
|
datastream << model_->GetGroupBy();
|
||||||
s.setValue("version", u"1"_s);
|
s.setValue("version", u"1"_s);
|
||||||
s.setValue(name, buffer);
|
s.setValue(QUrl::toPercentEncoding(name), buffer);
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
UpdateGroupByActions();
|
UpdateGroupByActions();
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ CollectionLibrary::CollectionLibrary(const SharedPtr<Database> database,
|
|||||||
|
|
||||||
model_ = new CollectionModel(backend_, albumcover_loader, this);
|
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();
|
ReloadSettings();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Strawberry Music Player
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
|
|
||||||
#include "includes/scoped_ptr.h"
|
#include "includes/scoped_ptr.h"
|
||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "constants/collectionsettings.h"
|
||||||
#include "core/logging.h"
|
#include "core/logging.h"
|
||||||
#include "core/standardpaths.h"
|
#include "core/standardpaths.h"
|
||||||
#include "core/database.h"
|
#include "core/database.h"
|
||||||
@@ -71,12 +72,12 @@
|
|||||||
#include "covermanager/albumcoverloaderoptions.h"
|
#include "covermanager/albumcoverloaderoptions.h"
|
||||||
#include "covermanager/albumcoverloaderresult.h"
|
#include "covermanager/albumcoverloaderresult.h"
|
||||||
#include "covermanager/albumcoverloader.h"
|
#include "covermanager/albumcoverloader.h"
|
||||||
#include "constants/collectionsettings.h"
|
|
||||||
|
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
const int CollectionModel::kPrettyCoverSize = 32;
|
const int CollectionModel::kPrettyCoverSize = 32;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char kPixmapDiskCacheDir[] = "pixmapcache";
|
constexpr char kPixmapDiskCacheDir[] = "pixmapcache";
|
||||||
constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists");
|
constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists");
|
||||||
@@ -88,7 +89,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
|
|||||||
albumcover_loader_(albumcover_loader),
|
albumcover_loader_(albumcover_loader),
|
||||||
dir_model_(new CollectionDirectoryModel(backend, this)),
|
dir_model_(new CollectionDirectoryModel(backend, this)),
|
||||||
filter_(new CollectionFilter(this)),
|
filter_(new CollectionFilter(this)),
|
||||||
timer_reload_(new QTimer(this)),
|
|
||||||
timer_update_(new QTimer(this)),
|
timer_update_(new QTimer(this)),
|
||||||
icon_artist_(IconLoader::Load(u"folder-sound"_s)),
|
icon_artist_(IconLoader::Load(u"folder-sound"_s)),
|
||||||
use_disk_cache_(false),
|
use_disk_cache_(false),
|
||||||
@@ -130,10 +130,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
|
|||||||
backend_->UpdateTotalArtistCountAsync();
|
backend_->UpdateTotalArtistCountAsync();
|
||||||
backend_->UpdateTotalAlbumCountAsync();
|
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_->setSingleShot(false);
|
||||||
timer_update_->setInterval(20ms);
|
timer_update_->setInterval(20ms);
|
||||||
QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate);
|
QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate);
|
||||||
@@ -191,13 +187,9 @@ void CollectionModel::EndReset() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollectionModel::Reload() {
|
void CollectionModel::ResetInternal() {
|
||||||
|
|
||||||
loading_ = true;
|
loading_ = true;
|
||||||
if (timer_reload_->isActive()) {
|
|
||||||
timer_reload_->stop();
|
|
||||||
}
|
|
||||||
updates_.clear();
|
|
||||||
|
|
||||||
options_active_ = options_current_;
|
options_active_ = options_current_;
|
||||||
|
|
||||||
@@ -211,22 +203,15 @@ void CollectionModel::Reload() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollectionModel::ScheduleReset() {
|
|
||||||
|
|
||||||
if (!timer_reload_->isActive()) {
|
|
||||||
timer_reload_->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionModel::ReloadSettings() {
|
void CollectionModel::ReloadSettings() {
|
||||||
|
|
||||||
Settings settings;
|
Settings settings;
|
||||||
settings.beginGroup(CollectionSettings::kSettingsGroup);
|
settings.beginGroup(CollectionSettings::kSettingsGroup);
|
||||||
const bool show_pretty_covers = settings.value(CollectionSettings::kPrettyCovers, true).toBool();
|
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 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();
|
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
|
||||||
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
|
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 ||
|
if (show_pretty_covers != options_current_.show_pretty_covers ||
|
||||||
show_dividers != options_current_.show_dividers ||
|
show_dividers != options_current_.show_dividers ||
|
||||||
show_various_artists != options_current_.show_various_artists ||
|
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_pretty_covers = show_pretty_covers;
|
||||||
options_current_.show_dividers = show_dividers;
|
options_current_.show_dividers = show_dividers;
|
||||||
options_current_.show_various_artists = show_various_artists;
|
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();
|
ScheduleReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,10 +408,15 @@ void CollectionModel::RemoveSongs(const SongList &songs) {
|
|||||||
|
|
||||||
void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) {
|
void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) {
|
||||||
|
|
||||||
for (qint64 i = 0; i < songs.count(); i += 400LL) {
|
if (type == CollectionModelUpdate::Type::Reset) {
|
||||||
const qint64 number = std::min(songs.count() - i, 400LL);
|
updates_.enqueue(CollectionModelUpdate(type));
|
||||||
const SongList songs_to_queue = songs.mid(i, number);
|
}
|
||||||
updates_.enqueue(CollectionModelUpdate(type, songs_to_queue));
|
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()) {
|
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) {
|
void CollectionModel::ScheduleAddSongs(const SongList &songs) {
|
||||||
|
|
||||||
ScheduleUpdate(CollectionModelUpdate::Type::Add, songs);
|
ScheduleUpdate(CollectionModelUpdate::Type::Add, songs);
|
||||||
@@ -465,6 +465,9 @@ void CollectionModel::ProcessUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (update.type) {
|
switch (update.type) {
|
||||||
|
case CollectionModelUpdate::Type::Reset:
|
||||||
|
ResetInternal();
|
||||||
|
break;
|
||||||
case CollectionModelUpdate::Type::AddReAddOrUpdate:
|
case CollectionModelUpdate::Type::AddReAddOrUpdate:
|
||||||
AddReAddOrUpdateSongsInternal(update.songs);
|
AddReAddOrUpdateSongsInternal(update.songs);
|
||||||
break;
|
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
|
// 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 (!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.
|
// 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.
|
// 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;
|
QString divider_key;
|
||||||
if (options_active_.show_dividers && container_level == 0) {
|
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_key.isEmpty()) {
|
||||||
if (!divider_nodes_.contains(divider_key)) {
|
if (!divider_nodes_.contains(divider_key)) {
|
||||||
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
|
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_level = container_level;
|
||||||
item->container_key = container_key;
|
item->container_key = container_key;
|
||||||
item->display_text = DisplayText(group_by, song);
|
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()) {
|
if (!divider_key.isEmpty()) {
|
||||||
item->sort_text.prepend(divider_key + QLatin1Char(' '));
|
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) {
|
switch (group_by) {
|
||||||
case GroupBy::AlbumArtist:
|
case GroupBy::AlbumArtist:
|
||||||
return SortTextForArtist(song.effective_albumartist(), sort_skips_articles);
|
return SortTextForName(song.effective_albumartistsort(), sort_skip_articles_for_artists);
|
||||||
case GroupBy::Artist:
|
case GroupBy::Artist:
|
||||||
return SortTextForArtist(song.artist(), sort_skips_articles);
|
return SortTextForName(song.effective_artistsort(), sort_skip_articles_for_artists);
|
||||||
case GroupBy::Album:
|
case GroupBy::Album:
|
||||||
return SortText(song.album());
|
return SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
|
||||||
case GroupBy::AlbumDisc:
|
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:
|
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:
|
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:
|
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:
|
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:
|
case GroupBy::Disc:
|
||||||
return SortTextForNumber(std::max(0, song.disc()));
|
return SortTextForNumber(std::max(0, song.disc()));
|
||||||
case GroupBy::Year:
|
case GroupBy::Year:
|
||||||
@@ -1094,13 +1100,13 @@ QString CollectionModel::SortText(const GroupBy group_by, const Song &song, cons
|
|||||||
case GroupBy::OriginalYear:
|
case GroupBy::OriginalYear:
|
||||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
|
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
|
||||||
case GroupBy::Genre:
|
case GroupBy::Genre:
|
||||||
return SortTextForArtist(song.genre(), sort_skips_articles);
|
return SortText(song.genre());
|
||||||
case GroupBy::Composer:
|
case GroupBy::Composer:
|
||||||
return SortTextForArtist(song.composer(), sort_skips_articles);
|
return SortTextForName(song.effective_composersort(), sort_skip_articles_for_artists);
|
||||||
case GroupBy::Performer:
|
case GroupBy::Performer:
|
||||||
return SortTextForArtist(song.performer(), sort_skips_articles);
|
return SortTextForName(song.effective_performersort(), sort_skip_articles_for_artists);
|
||||||
case GroupBy::Grouping:
|
case GroupBy::Grouping:
|
||||||
return SortTextForArtist(song.grouping(), sort_skips_articles);
|
return SortText(song.grouping());
|
||||||
case GroupBy::FileType:
|
case GroupBy::FileType:
|
||||||
return song.TextForFiletype();
|
return song.TextForFiletype();
|
||||||
case GroupBy::Format:
|
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);
|
return sort_skip_articles ? SkipArticles(SortText(name)) : SortText(name);
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
bool CollectionModel::IsSongTitleDataChanged(const Song &song1, const Song &song2) {
|
||||||
|
|
||||||
return song1.url() != song2.url() ||
|
return song1.url() != song2.url() ||
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Strawberry Music Player
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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_dividers(true),
|
||||||
show_pretty_covers(true),
|
show_pretty_covers(true),
|
||||||
show_various_artists(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) {}
|
separate_albums_by_grouping(false) {}
|
||||||
|
|
||||||
Grouping group_by;
|
Grouping group_by;
|
||||||
bool show_dividers;
|
bool show_dividers;
|
||||||
bool show_pretty_covers;
|
bool show_pretty_covers;
|
||||||
bool show_various_artists;
|
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;
|
bool separate_albums_by_grouping;
|
||||||
CollectionFilterOptions filter_options;
|
CollectionFilterOptions filter_options;
|
||||||
};
|
};
|
||||||
@@ -176,20 +178,21 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||||
|
|
||||||
// Utility functions for manipulating text
|
// 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 TextOrUnknown(const QString &text);
|
||||||
static QString PrettyYearAlbum(const int year, const QString &album);
|
static QString PrettyYearAlbum(const int year, const QString &album);
|
||||||
static QString PrettyAlbumDisc(const QString &album, const int disc);
|
static QString PrettyAlbumDisc(const QString &album, const int disc);
|
||||||
static QString PrettyYearAlbumDisc(const int year, 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 PrettyDisc(const int disc);
|
||||||
static QString PrettyFormat(const Song &song);
|
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 SortText(QString text);
|
||||||
|
static QString SortTextForName(const QString &name, const bool sort_skip_articles);
|
||||||
static QString SortTextForNumber(const int number);
|
static QString SortTextForNumber(const int number);
|
||||||
static QString SortTextForArtist(QString artist, const bool skip_articles);
|
|
||||||
static QString SortTextForSong(const Song &song);
|
static QString SortTextForSong(const Song &song);
|
||||||
static QString SortTextForYear(const int year);
|
static QString SortTextForYear(const int year);
|
||||||
static QString SortTextForBitrate(const int bitrate);
|
static QString SortTextForBitrate(const int bitrate);
|
||||||
|
static QString SkipArticles(QString name);
|
||||||
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
|
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
|
||||||
QString ContainerKey(const GroupBy group_by, const Song &song, bool &has_unique_album_identifier) const;
|
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;
|
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 ScheduleAddSongs(const SongList &songs);
|
||||||
void ScheduleUpdateSongs(const SongList &songs);
|
void ScheduleUpdateSongs(const SongList &songs);
|
||||||
void ScheduleRemoveSongs(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);
|
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void Reload();
|
void ResetInternal();
|
||||||
void ScheduleReset();
|
void ScheduleReset();
|
||||||
void ProcessUpdate();
|
void ProcessUpdate();
|
||||||
void LoadSongsFromSqlAsyncFinished();
|
void LoadSongsFromSqlAsyncFinished();
|
||||||
@@ -278,7 +281,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||||||
const SharedPtr<AlbumCoverLoader> albumcover_loader_;
|
const SharedPtr<AlbumCoverLoader> albumcover_loader_;
|
||||||
CollectionDirectoryModel *dir_model_;
|
CollectionDirectoryModel *dir_model_;
|
||||||
CollectionFilter *filter_;
|
CollectionFilter *filter_;
|
||||||
QTimer *timer_reload_;
|
|
||||||
QTimer *timer_update_;
|
QTimer *timer_update_;
|
||||||
|
|
||||||
QPixmap pixmap_no_cover_;
|
QPixmap pixmap_no_cover_;
|
||||||
|
|||||||
@@ -25,12 +25,13 @@
|
|||||||
class CollectionModelUpdate {
|
class CollectionModelUpdate {
|
||||||
public:
|
public:
|
||||||
enum class Type {
|
enum class Type {
|
||||||
|
Reset,
|
||||||
AddReAddOrUpdate,
|
AddReAddOrUpdate,
|
||||||
Add,
|
Add,
|
||||||
Update,
|
Update,
|
||||||
Remove,
|
Remove,
|
||||||
};
|
};
|
||||||
explicit CollectionModelUpdate(const Type _type, const SongList &_songs);
|
explicit CollectionModelUpdate(const Type _type, const SongList &_songs = SongList());
|
||||||
Type type;
|
Type type;
|
||||||
SongList songs;
|
SongList songs;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ CollectionPlaylistItem::CollectionPlaylistItem(const Song::Source source) : Play
|
|||||||
|
|
||||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
|
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
|
||||||
|
|
||||||
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
|
|
||||||
|
|
||||||
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
|
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
|
||||||
|
|
||||||
int col = 0;
|
int col = 0;
|
||||||
@@ -62,7 +60,7 @@ void CollectionPlaylistItem::Reload() {
|
|||||||
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
|
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
|
||||||
return;
|
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) {
|
void CollectionPlaylistItem::SetArtManual(const QUrl &cover_url) {
|
||||||
|
|
||||||
song_.set_art_manual(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::Source source);
|
||||||
explicit CollectionPlaylistItem(const Song &song);
|
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;
|
bool InitFromQuery(const SqlRow &query) override;
|
||||||
void Reload() 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;
|
void SetArtManual(const QUrl &cover_url) override;
|
||||||
|
|
||||||
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QVariant DatabaseValue(const DatabaseColumn database_column) const override;
|
QVariant DatabaseValue(const DatabaseColumn database_column) const override;
|
||||||
Song DatabaseSongMetadata() const override { return Song(source_); }
|
Song DatabaseSongMetadata() const override { return Song(source_); }
|
||||||
|
|||||||
@@ -65,10 +65,8 @@
|
|||||||
#include "collectionitem.h"
|
#include "collectionitem.h"
|
||||||
#include "collectionitemdelegate.h"
|
#include "collectionitemdelegate.h"
|
||||||
#include "collectionview.h"
|
#include "collectionview.h"
|
||||||
#ifndef Q_OS_WIN32
|
#include "device/devicemanager.h"
|
||||||
# include "device/devicemanager.h"
|
#include "device/devicestatefiltermodel.h"
|
||||||
# include "device/devicestatefiltermodel.h"
|
|
||||||
#endif
|
|
||||||
#include "dialogs/edittagdialog.h"
|
#include "dialogs/edittagdialog.h"
|
||||||
#include "dialogs/deleteconfirmationdialog.h"
|
#include "dialogs/deleteconfirmationdialog.h"
|
||||||
#include "organize/organizedialog.h"
|
#include "organize/organizedialog.h"
|
||||||
@@ -95,9 +93,7 @@ CollectionView::CollectionView(QWidget *parent)
|
|||||||
action_open_in_new_playlist_(nullptr),
|
action_open_in_new_playlist_(nullptr),
|
||||||
action_organize_(nullptr),
|
action_organize_(nullptr),
|
||||||
action_search_for_this_(nullptr),
|
action_search_for_this_(nullptr),
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
action_copy_to_device_(nullptr),
|
action_copy_to_device_(nullptr),
|
||||||
#endif
|
|
||||||
action_edit_track_(nullptr),
|
action_edit_track_(nullptr),
|
||||||
action_edit_tracks_(nullptr),
|
action_edit_tracks_(nullptr),
|
||||||
action_rescan_songs_(nullptr),
|
action_rescan_songs_(nullptr),
|
||||||
@@ -417,9 +413,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
|
|
||||||
context_menu_->addSeparator();
|
context_menu_->addSeparator();
|
||||||
action_organize_ = context_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &CollectionView::Organize);
|
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);
|
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);
|
action_delete_files_ = context_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &CollectionView::Delete);
|
||||||
|
|
||||||
context_menu_->addSeparator();
|
context_menu_->addSeparator();
|
||||||
@@ -439,10 +433,8 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
|
|
||||||
context_menu_->addMenu(filter_widget_->menu());
|
context_menu_->addMenu(filter_widget_->menu());
|
||||||
|
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
action_copy_to_device_->setDisabled(device_manager_->connected_devices_model()->rowCount() == 0);
|
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);
|
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_rescan_songs_->setEnabled(regular_editable > 0);
|
||||||
|
|
||||||
action_organize_->setVisible(regular_elements == regular_editable);
|
action_organize_->setVisible(regular_elements == regular_editable);
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
action_copy_to_device_->setVisible(regular_elements == regular_editable);
|
action_copy_to_device_->setVisible(regular_elements == regular_editable);
|
||||||
#endif
|
|
||||||
|
|
||||||
action_delete_files_->setVisible(delete_files_);
|
action_delete_files_->setVisible(delete_files_);
|
||||||
|
|
||||||
@@ -492,9 +482,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
|
|
||||||
// only when all selected items are editable
|
// only when all selected items are editable
|
||||||
action_organize_->setEnabled(regular_elements == regular_editable);
|
action_organize_->setEnabled(regular_elements == regular_editable);
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
|
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
|
||||||
#endif
|
|
||||||
|
|
||||||
action_delete_files_->setEnabled(delete_files_);
|
action_delete_files_->setEnabled(delete_files_);
|
||||||
|
|
||||||
@@ -759,7 +747,6 @@ void CollectionView::RescanSongs() {
|
|||||||
|
|
||||||
void CollectionView::CopyToDevice() {
|
void CollectionView::CopyToDevice() {
|
||||||
|
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
if (!organize_dialog_) {
|
if (!organize_dialog_) {
|
||||||
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
|
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
|
||||||
}
|
}
|
||||||
@@ -768,7 +755,6 @@ void CollectionView::CopyToDevice() {
|
|||||||
organize_dialog_->SetCopy(true);
|
organize_dialog_->SetCopy(true);
|
||||||
organize_dialog_->SetSongs(GetSelectedSongs());
|
organize_dialog_->SetSongs(GetSelectedSongs());
|
||||||
organize_dialog_->show();
|
organize_dialog_->show();
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ class CollectionView : public AutoExpandingTreeView {
|
|||||||
void DeleteFilesFinished(const SongList &songs_with_errors);
|
void DeleteFilesFinished(const SongList &songs_with_errors);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void RecheckIsEmpty();
|
|
||||||
void SetShowInVarious(const bool on);
|
void SetShowInVarious(const bool on);
|
||||||
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
|
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
|
||||||
void SaveContainerPath(const QModelIndex &child);
|
void SaveContainerPath(const QModelIndex &child);
|
||||||
@@ -176,9 +175,7 @@ class CollectionView : public AutoExpandingTreeView {
|
|||||||
QAction *action_organize_;
|
QAction *action_organize_;
|
||||||
QAction *action_search_for_this_;
|
QAction *action_search_for_this_;
|
||||||
|
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
QAction *action_copy_to_device_;
|
QAction *action_copy_to_device_;
|
||||||
#endif
|
|
||||||
QAction *action_edit_track_;
|
QAction *action_edit_track_;
|
||||||
QAction *action_edit_tracks_;
|
QAction *action_edit_tracks_;
|
||||||
QAction *action_rescan_songs_;
|
QAction *action_rescan_songs_;
|
||||||
|
|||||||
@@ -999,6 +999,18 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
|
|||||||
changes << u"file path"_s;
|
changes << u"file path"_s;
|
||||||
notify_new = true;
|
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()) {
|
if (matching_song.fingerprint() != new_song.fingerprint()) {
|
||||||
changes << u"fingerprint"_s;
|
changes << u"fingerprint"_s;
|
||||||
notify_new = true;
|
notify_new = true;
|
||||||
@@ -1034,6 +1046,9 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
|
|||||||
if (matching_song.mtime() != new_song.mtime()) {
|
if (matching_song.mtime() != new_song.mtime()) {
|
||||||
changes << u"mtime"_s;
|
changes << u"mtime"_s;
|
||||||
}
|
}
|
||||||
|
if (matching_song.ctime() != new_song.ctime()) {
|
||||||
|
changes << u"ctime"_s;
|
||||||
|
}
|
||||||
|
|
||||||
if (changes.isEmpty()) {
|
if (changes.isEmpty()) {
|
||||||
qLog(Debug) << "Song" << file << "unchanged.";
|
qLog(Debug) << "Song" << file << "unchanged.";
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <QUrl>
|
||||||
#include <QIODevice>
|
#include <QIODevice>
|
||||||
#include <QDataStream>
|
#include <QDataStream>
|
||||||
#include <QKeySequence>
|
#include <QKeySequence>
|
||||||
@@ -167,14 +168,20 @@ void SavedGroupingManager::UpdateModel() {
|
|||||||
if (version == 1) {
|
if (version == 1) {
|
||||||
QStringList saved = s.childKeys();
|
QStringList saved = s.childKeys();
|
||||||
for (int i = 0; i < saved.size(); ++i) {
|
for (int i = 0; i < saved.size(); ++i) {
|
||||||
if (saved.at(i) == "version"_L1) continue;
|
const QString &name = saved.at(i);
|
||||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
if (name == "version"_L1) continue;
|
||||||
|
QByteArray bytes = s.value(name).toByteArray();
|
||||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||||
CollectionModel::Grouping g;
|
CollectionModel::Grouping g;
|
||||||
ds >> g;
|
ds >> g;
|
||||||
|
|
||||||
QList<QStandardItem*> list;
|
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.first))
|
||||||
<< new QStandardItem(GroupByToString(g.second))
|
<< new QStandardItem(GroupByToString(g.second))
|
||||||
<< new QStandardItem(GroupByToString(g.third));
|
<< new QStandardItem(GroupByToString(g.third));
|
||||||
@@ -185,8 +192,9 @@ void SavedGroupingManager::UpdateModel() {
|
|||||||
else {
|
else {
|
||||||
QStringList saved = s.childKeys();
|
QStringList saved = s.childKeys();
|
||||||
for (int i = 0; i < saved.size(); ++i) {
|
for (int i = 0; i < saved.size(); ++i) {
|
||||||
if (saved.at(i) == "version"_L1) continue;
|
const QString &name = saved.at(i);
|
||||||
s.remove(saved.at(i));
|
if (name == "version"_L1) continue;
|
||||||
|
s.remove(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
@@ -202,7 +210,7 @@ void SavedGroupingManager::Remove() {
|
|||||||
for (const QModelIndex &idx : indexes) {
|
for (const QModelIndex &idx : indexes) {
|
||||||
if (idx.isValid()) {
|
if (idx.isValid()) {
|
||||||
qLog(Debug) << "Remove saved grouping: " << model_->item(idx.row(), 0)->text();
|
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();
|
s.endGroup();
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ constexpr char kOutputU[] = "Output";
|
|||||||
constexpr char kDevice[] = "device";
|
constexpr char kDevice[] = "device";
|
||||||
constexpr char kDeviceU[] = "Device";
|
constexpr char kDeviceU[] = "Device";
|
||||||
constexpr char kALSAPlugin[] = "alsaplugin";
|
constexpr char kALSAPlugin[] = "alsaplugin";
|
||||||
|
constexpr char kPlaybin3[] = "playbin3";
|
||||||
constexpr char kExclusiveMode[] = "exclusive_mode";
|
constexpr char kExclusiveMode[] = "exclusive_mode";
|
||||||
constexpr char kVolumeControl[] = "volume_control";
|
constexpr char kVolumeControl[] = "volume_control";
|
||||||
constexpr char kChannelsEnabled[] = "channels_enabled";
|
constexpr char kChannelsEnabled[] = "channels_enabled";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Strawberry Music Player
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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 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 kAutoOpen[] = "auto_open";
|
||||||
constexpr char kShowDividers[] = "show_dividers";
|
constexpr char kShowDividers[] = "show_dividers";
|
||||||
constexpr char kPrettyCovers[] = "pretty_covers";
|
constexpr char kPrettyCovers[] = "pretty_covers";
|
||||||
constexpr char kVariousArtists[] = "various_artists";
|
constexpr char kVariousArtists[] = "various_artists";
|
||||||
constexpr char kSortSkipsArticles[] = "sort_skips_articles";
|
constexpr char kSkipArticlesForArtists[] = "skip_articles_for_artists";
|
||||||
constexpr char kStartupScan[] = "startup_scan";
|
constexpr char kSkipArticlesForAlbums[] = "skip_articles_for_albums";
|
||||||
constexpr char kMonitor[] = "monitor";
|
constexpr char kShowSortText[] = "show_sort_text";
|
||||||
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 kSettingsCacheSize[] = "cache_size";
|
constexpr char kSettingsCacheSize[] = "cache_size";
|
||||||
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
|
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
|
||||||
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";
|
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ constexpr char kAllFilesFilterSpec[] = QT_TRANSLATE_NOOP("FileFilter", "All File
|
|||||||
constexpr char kFileFilter[] =
|
constexpr char kFileFilter[] =
|
||||||
"*.wav *.flac *.wv *.ogg *.oga *.opus *.spx *.ape *.mpc "
|
"*.wav *.flac *.wv *.ogg *.oga *.opus *.spx *.ape *.mpc "
|
||||||
"*.mp2 *.mp3 *.m4a *.mp4 *.aac *.asf *.asx *.wma "
|
"*.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 "
|
"*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini "
|
||||||
"*.ac3 *.dts "
|
"*.ac3 *.dts "
|
||||||
"*.mod *.s3m *.xm *.it"
|
"*.mod *.s3m *.xm *.it "
|
||||||
"*.spc *.vgm";
|
"*.spc *.vgm";
|
||||||
|
|
||||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
|
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 kEnabled[] = "enabled";
|
||||||
|
|
||||||
|
constexpr char kStatusDisplayType[] = "StatusDisplayType";
|
||||||
|
|
||||||
|
enum class StatusDisplayType {
|
||||||
|
App = 0,
|
||||||
|
Artist,
|
||||||
|
Song
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
#endif // NOTIFICATIONSSETTINGS_H
|
#endif // NOTIFICATIONSSETTINGS_H
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ constexpr char kAlbumsSearchLimit[] = "albumssearchlimit";
|
|||||||
constexpr char kSongsSearchLimit[] = "songssearchlimit";
|
constexpr char kSongsSearchLimit[] = "songssearchlimit";
|
||||||
constexpr char kBase64Secret[] = "base64secret";
|
constexpr char kBase64Secret[] = "base64secret";
|
||||||
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
|
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
|
||||||
|
constexpr char kRemoveRemastered[] = "remove_remastered";
|
||||||
|
|
||||||
constexpr char kUserId[] = "user_id";
|
constexpr char kUserId[] = "user_id";
|
||||||
constexpr char kCredentialsId[] = "credentials_id";
|
constexpr char kCredentialsId[] = "credentials_id";
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ constexpr char kAlbumsSearchLimit[] = "albumssearchlimit";
|
|||||||
constexpr char kSongsSearchLimit[] = "songssearchlimit";
|
constexpr char kSongsSearchLimit[] = "songssearchlimit";
|
||||||
constexpr char kFetchAlbums[] = "fetchalbums";
|
constexpr char kFetchAlbums[] = "fetchalbums";
|
||||||
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
|
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
|
||||||
|
constexpr char kRemoveRemastered[] = "remove_remastered";
|
||||||
|
|
||||||
constexpr char kAccessToken[] = "access_token";
|
constexpr char kAccessToken[] = "access_token";
|
||||||
constexpr char kRefreshToken[] = "refresh_token";
|
constexpr char kRefreshToken[] = "refresh_token";
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
|
|||||||
constexpr char kCoverSize[] = "coversize";
|
constexpr char kCoverSize[] = "coversize";
|
||||||
constexpr char kStreamUrl[] = "streamurl";
|
constexpr char kStreamUrl[] = "streamurl";
|
||||||
constexpr char kAlbumExplicit[] = "album_explicit";
|
constexpr char kAlbumExplicit[] = "album_explicit";
|
||||||
|
constexpr char kRemoveRemastered[] = "remove_remastered";
|
||||||
|
|
||||||
enum class StreamUrlMethod {
|
enum class StreamUrlMethod {
|
||||||
StreamUrl,
|
StreamUrl,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Strawberry Music Player
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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_samplerate_title_(new QLabel(this)),
|
||||||
label_bitdepth_title_(new QLabel(this)),
|
label_bitdepth_title_(new QLabel(this)),
|
||||||
label_bitrate_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_filetype_(new QLabel(this)),
|
||||||
label_length_(new QLabel(this)),
|
label_length_(new QLabel(this)),
|
||||||
label_samplerate_(new QLabel(this)),
|
label_samplerate_(new QLabel(this)),
|
||||||
label_bitdepth_(new QLabel(this)),
|
label_bitdepth_(new QLabel(this)),
|
||||||
label_bitrate_(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_tried_(false),
|
||||||
lyrics_id_(-1) {
|
lyrics_id_(-1) {
|
||||||
|
|
||||||
@@ -166,24 +162,18 @@ ContextView::ContextView(QWidget *parent)
|
|||||||
label_samplerate_title_->setText(tr("Samplerate"));
|
label_samplerate_title_->setText(tr("Samplerate"));
|
||||||
label_bitdepth_title_->setText(tr("Bit depth"));
|
label_bitdepth_title_->setText(tr("Bit depth"));
|
||||||
label_bitrate_title_->setText(tr("Bitrate"));
|
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_filetype_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||||
label_length_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
label_length_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||||
label_samplerate_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
label_samplerate_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||||
label_bitdepth_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
label_bitdepth_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||||
label_bitrate_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_filetype_->setWordWrap(true);
|
||||||
label_length_->setWordWrap(true);
|
label_length_->setWordWrap(true);
|
||||||
label_samplerate_->setWordWrap(true);
|
label_samplerate_->setWordWrap(true);
|
||||||
label_bitdepth_->setWordWrap(true);
|
label_bitdepth_->setWordWrap(true);
|
||||||
label_bitrate_->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_->setContentsMargins(0, 0, 0, 0);
|
||||||
layout_play_data_->addWidget(label_filetype_title_, 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_title_, 4, 0);
|
||||||
layout_play_data_->addWidget(label_bitrate_, 4, 1);
|
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_);
|
widget_play_data_->setLayout(layout_play_data_);
|
||||||
|
|
||||||
textedit_play_lyrics_->setReadOnly(true);
|
textedit_play_lyrics_->setReadOnly(true);
|
||||||
@@ -218,17 +203,13 @@ ContextView::ContextView(QWidget *parent)
|
|||||||
<< label_length_title_
|
<< label_length_title_
|
||||||
<< label_samplerate_title_
|
<< label_samplerate_title_
|
||||||
<< label_bitdepth_title_
|
<< label_bitdepth_title_
|
||||||
<< label_bitrate_title_
|
<< label_bitrate_title_;
|
||||||
<< label_ebur128_integrated_loudness_title_
|
|
||||||
<< label_ebur128_loudness_range_title_;
|
|
||||||
|
|
||||||
labels_play_data_ << label_filetype_
|
labels_play_data_ << label_filetype_
|
||||||
<< label_length_
|
<< label_length_
|
||||||
<< label_samplerate_
|
<< label_samplerate_
|
||||||
<< label_bitdepth_
|
<< label_bitdepth_
|
||||||
<< label_bitrate_
|
<< label_bitrate_;
|
||||||
<< label_ebur128_integrated_loudness_
|
|
||||||
<< label_ebur128_loudness_range_;
|
|
||||||
|
|
||||||
labels_play_all_ = labels_play_ << labels_play_data_;
|
labels_play_all_ = labels_play_ << labels_play_data_;
|
||||||
|
|
||||||
@@ -396,7 +377,7 @@ void ContextView::UpdateNoSong() {
|
|||||||
|
|
||||||
void ContextView::NoSong() {
|
void ContextView::NoSong() {
|
||||||
|
|
||||||
if (!widget_album_->isVisible()) {
|
if (!widget_album_->isVisibleTo(this)) {
|
||||||
widget_album_->show();
|
widget_album_->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,11 +421,11 @@ void ContextView::SetSong() {
|
|||||||
label_stop_summary_->clear();
|
label_stop_summary_->clear();
|
||||||
|
|
||||||
bool widget_album_changed = !song_prev_.is_valid();
|
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_->show();
|
||||||
widget_album_changed = true;
|
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_->hide();
|
||||||
widget_album_changed = true;
|
widget_album_changed = true;
|
||||||
}
|
}
|
||||||
@@ -493,26 +474,6 @@ void ContextView::SetSong() {
|
|||||||
label_bitrate_->show();
|
label_bitrate_->show();
|
||||||
SetLabelText(label_bitrate_, song_playing_.bitrate(), tr("kbps"));
|
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);
|
spacer_play_data_->changeSize(20, 20, QSizePolicy::Fixed);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -522,8 +483,6 @@ void ContextView::SetSong() {
|
|||||||
label_samplerate_->clear();
|
label_samplerate_->clear();
|
||||||
label_bitdepth_->clear();
|
label_bitdepth_->clear();
|
||||||
label_bitrate_->clear();
|
label_bitrate_->clear();
|
||||||
label_ebur128_integrated_loudness_->clear();
|
|
||||||
label_ebur128_loudness_range_->clear();
|
|
||||||
spacer_play_data_->changeSize(0, 0, QSizePolicy::Fixed);
|
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"));
|
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;
|
song_playing_ = song;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Strawberry Music Player
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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_bitdepth_title_;
|
||||||
QLabel *label_bitrate_title_;
|
QLabel *label_bitrate_title_;
|
||||||
|
|
||||||
QLabel *label_ebur128_integrated_loudness_title_;
|
|
||||||
QLabel *label_ebur128_loudness_range_title_;
|
|
||||||
|
|
||||||
QLabel *label_filetype_;
|
QLabel *label_filetype_;
|
||||||
QLabel *label_length_;
|
QLabel *label_length_;
|
||||||
QLabel *label_samplerate_;
|
QLabel *label_samplerate_;
|
||||||
QLabel *label_bitdepth_;
|
QLabel *label_bitdepth_;
|
||||||
QLabel *label_bitrate_;
|
QLabel *label_bitrate_;
|
||||||
|
|
||||||
QLabel *label_ebur128_integrated_loudness_;
|
|
||||||
QLabel *label_ebur128_loudness_range_;
|
|
||||||
|
|
||||||
Song song_playing_;
|
Song song_playing_;
|
||||||
Song song_prev_;
|
Song song_prev_;
|
||||||
QImage image_original_;
|
QImage image_original_;
|
||||||
|
|||||||
@@ -77,7 +77,6 @@
|
|||||||
|
|
||||||
#include "scrobbler/audioscrobbler.h"
|
#include "scrobbler/audioscrobbler.h"
|
||||||
#include "scrobbler/lastfmscrobbler.h"
|
#include "scrobbler/lastfmscrobbler.h"
|
||||||
#include "scrobbler/librefmscrobbler.h"
|
|
||||||
#include "scrobbler/listenbrainzscrobbler.h"
|
#include "scrobbler/listenbrainzscrobbler.h"
|
||||||
#include "scrobbler/lastfmimport.h"
|
#include "scrobbler/lastfmimport.h"
|
||||||
#ifdef HAVE_SUBSONIC
|
#ifdef HAVE_SUBSONIC
|
||||||
@@ -206,7 +205,6 @@ class ApplicationImpl {
|
|||||||
scrobbler_([app]() {
|
scrobbler_([app]() {
|
||||||
AudioScrobbler *scrobbler = new AudioScrobbler(app);
|
AudioScrobbler *scrobbler = new AudioScrobbler(app);
|
||||||
scrobbler->AddService(make_shared<LastFMScrobbler>(scrobbler->settings(), app->network()));
|
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()));
|
scrobbler->AddService(make_shared<ListenBrainzScrobbler>(scrobbler->settings(), app->network()));
|
||||||
#ifdef HAVE_SUBSONIC
|
#ifdef HAVE_SUBSONIC
|
||||||
scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->network(), app->streaming_services()->Service<SubsonicService>(), app));
|
scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->network(), app->streaming_services()->Service<SubsonicService>(), app));
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
const int Database::kSchemaVersion = 20;
|
const int Database::kSchemaVersion = 21;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char kDatabaseFilename[] = "strawberry.db";
|
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.
|
// We allow a magic value in the schema files to update all songs tables at once.
|
||||||
if (command.contains(QLatin1String(kMagicAllSongsTables))) {
|
if (command.contains(QLatin1String(kMagicAllSongsTables))) {
|
||||||
for (const QString &table : song_tables) {
|
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;
|
qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables;
|
||||||
QString new_command(command);
|
QString new_command(command);
|
||||||
new_command.replace(QLatin1String(kMagicAllSongsTables), table);
|
new_command.replace(QLatin1String(kMagicAllSongsTables), table);
|
||||||
|
|||||||
@@ -157,12 +157,16 @@ void HttpBaseRequest::HandleSSLErrors(const QList<QSslError> &ssl_errors) {
|
|||||||
HttpBaseRequest::ReplyDataResult HttpBaseRequest::GetReplyData(QNetworkReply *reply) {
|
HttpBaseRequest::ReplyDataResult HttpBaseRequest::GetReplyData(QNetworkReply *reply) {
|
||||||
|
|
||||||
if (reply->error() != QNetworkReply::NoError) {
|
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()));
|
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
|
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
|
||||||
const int http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
const int http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
if (http_status_code < 200 || http_status_code > 207) {
|
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));
|
return ReplyDataResult(ErrorCode::HttpError, QStringLiteral("Received HTTP code %1").arg(http_status_code));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -156,10 +156,8 @@
|
|||||||
#include "lyrics/lyricsproviders.h"
|
#include "lyrics/lyricsproviders.h"
|
||||||
#include "device/devicemanager.h"
|
#include "device/devicemanager.h"
|
||||||
#include "device/devicestatefiltermodel.h"
|
#include "device/devicestatefiltermodel.h"
|
||||||
#ifndef Q_OS_WIN32
|
#include "device/deviceview.h"
|
||||||
# include "device/deviceview.h"
|
#include "device/deviceviewcontainer.h"
|
||||||
# include "device/deviceviewcontainer.h"
|
|
||||||
#endif
|
|
||||||
#include "transcoder/transcodedialog.h"
|
#include "transcoder/transcodedialog.h"
|
||||||
#include "settings/settingsdialog.h"
|
#include "settings/settingsdialog.h"
|
||||||
#include "constants/behavioursettings.h"
|
#include "constants/behavioursettings.h"
|
||||||
@@ -175,6 +173,7 @@
|
|||||||
# include "constants/tidalsettings.h"
|
# include "constants/tidalsettings.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_SPOTIFY
|
#ifdef HAVE_SPOTIFY
|
||||||
|
# include "spotify/spotifyservice.h"
|
||||||
# include "constants/spotifysettings.h"
|
# include "constants/spotifysettings.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_QOBUZ
|
#ifdef HAVE_QOBUZ
|
||||||
@@ -280,7 +279,7 @@ constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-
|
|||||||
#endif // HAVE_QTSPARKLE
|
#endif // HAVE_QTSPARKLE
|
||||||
|
|
||||||
MainWindow::MainWindow(Application *app,
|
MainWindow::MainWindow(Application *app,
|
||||||
SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd,
|
SharedPtr<SystemTrayIcon> systemtrayicon, OSDBase *osd,
|
||||||
#ifdef HAVE_DISCORD_RPC
|
#ifdef HAVE_DISCORD_RPC
|
||||||
discord::RichPresence *discord_rich_presence,
|
discord::RichPresence *discord_rich_presence,
|
||||||
#endif
|
#endif
|
||||||
@@ -292,11 +291,14 @@ MainWindow::MainWindow(Application *app,
|
|||||||
thumbbar_(new Windows7ThumbBar(this)),
|
thumbbar_(new Windows7ThumbBar(this)),
|
||||||
#endif
|
#endif
|
||||||
app_(app),
|
app_(app),
|
||||||
tray_icon_(tray_icon),
|
systemtrayicon_(systemtrayicon),
|
||||||
osd_(osd),
|
osd_(osd),
|
||||||
#ifdef HAVE_DISCORD_RPC
|
#ifdef HAVE_DISCORD_RPC
|
||||||
discord_rich_presence_(discord_rich_presence),
|
discord_rich_presence_(discord_rich_presence),
|
||||||
#endif
|
#endif
|
||||||
|
error_dialog_([this]() {
|
||||||
|
return new ErrorDialog(this);
|
||||||
|
}),
|
||||||
console_([app, this]() {
|
console_([app, this]() {
|
||||||
Console *console = new Console(app->database());
|
Console *console = new Console(app->database());
|
||||||
QObject::connect(console, &Console::Error, this, &MainWindow::ShowErrorDialog);
|
QObject::connect(console, &Console::Error, this, &MainWindow::ShowErrorDialog);
|
||||||
@@ -310,9 +312,7 @@ MainWindow::MainWindow(Application *app,
|
|||||||
context_view_(new ContextView(this)),
|
context_view_(new ContextView(this)),
|
||||||
collection_view_(new CollectionViewContainer(this)),
|
collection_view_(new CollectionViewContainer(this)),
|
||||||
file_view_(new FileView(this)),
|
file_view_(new FileView(this)),
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
device_view_(new DeviceViewContainer(this)),
|
device_view_(new DeviceViewContainer(this)),
|
||||||
#endif
|
|
||||||
playlist_list_(new PlaylistListContainer(this)),
|
playlist_list_(new PlaylistListContainer(this)),
|
||||||
queue_view_(new QueueView(this)),
|
queue_view_(new QueueView(this)),
|
||||||
settings_dialog_(std::bind(&MainWindow::CreateSettingsDialog, this)),
|
settings_dialog_(std::bind(&MainWindow::CreateSettingsDialog, this)),
|
||||||
@@ -375,9 +375,7 @@ MainWindow::MainWindow(Application *app,
|
|||||||
playlist_move_to_collection_(nullptr),
|
playlist_move_to_collection_(nullptr),
|
||||||
playlist_open_in_browser_(nullptr),
|
playlist_open_in_browser_(nullptr),
|
||||||
playlist_organize_(nullptr),
|
playlist_organize_(nullptr),
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
playlist_copy_to_device_(nullptr),
|
playlist_copy_to_device_(nullptr),
|
||||||
#endif
|
|
||||||
playlist_delete_(nullptr),
|
playlist_delete_(nullptr),
|
||||||
playlist_queue_(nullptr),
|
playlist_queue_(nullptr),
|
||||||
playlist_queue_play_next_(nullptr),
|
playlist_queue_play_next_(nullptr),
|
||||||
@@ -409,7 +407,11 @@ MainWindow::MainWindow(Application *app,
|
|||||||
// Initialize the UI
|
// Initialize the UI
|
||||||
ui_->setupUi(this);
|
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);
|
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(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(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"));
|
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"));
|
ui_->tabs->AddTab(device_view_, u"devices"_s, IconLoader::Load(u"device"_s, true, 0, 32), tr("Devices"));
|
||||||
#endif
|
|
||||||
#ifdef HAVE_SUBSONIC
|
#ifdef HAVE_SUBSONIC
|
||||||
ui_->tabs->AddTab(subsonic_view_, u"subsonic"_s, IconLoader::Load(u"subsonic"_s, true, 0, 32), tr("Subsonic"));
|
ui_->tabs->AddTab(subsonic_view_, u"subsonic"_s, IconLoader::Load(u"subsonic"_s, true, 0, 32), tr("Subsonic"));
|
||||||
#endif
|
#endif
|
||||||
@@ -480,9 +480,7 @@ MainWindow::MainWindow(Application *app,
|
|||||||
|
|
||||||
collection_view_->view()->setModel(app_->collection()->model()->filter());
|
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());
|
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());
|
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());
|
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());
|
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::CopyToCollection, this, &MainWindow::CopyFilesToCollection);
|
||||||
QObject::connect(file_view_, &FileView::MoveToCollection, this, &MainWindow::MoveFilesToCollection);
|
QObject::connect(file_view_, &FileView::MoveToCollection, this, &MainWindow::MoveFilesToCollection);
|
||||||
QObject::connect(file_view_, &FileView::EditTags, this, &MainWindow::EditFileTags);
|
QObject::connect(file_view_, &FileView::EditTags, this, &MainWindow::EditFileTags);
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
QObject::connect(file_view_, &FileView::CopyToDevice, this, &MainWindow::CopyFilesToDevice);
|
QObject::connect(file_view_, &FileView::CopyToDevice, this, &MainWindow::CopyFilesToDevice);
|
||||||
#endif
|
|
||||||
file_view_->SetTaskManager(app_->task_manager());
|
file_view_->SetTaskManager(app_->task_manager());
|
||||||
|
|
||||||
// Action connections
|
// 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::triggered, this, &MainWindow::SearchCoverAutomatically);
|
||||||
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::toggled, this, &MainWindow::ToggleSearchCoverAuto);
|
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::toggled, this, &MainWindow::ToggleSearchCoverAuto);
|
||||||
|
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
// Devices connections
|
// Devices connections
|
||||||
QObject::connect(device_view_->view(), &DeviceView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
|
QObject::connect(device_view_->view(), &DeviceView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
|
||||||
#endif
|
|
||||||
|
|
||||||
// Collection filter widget
|
// Collection filter widget
|
||||||
QActionGroup *collection_view_group = new QActionGroup(this);
|
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_->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::OpenSettingsDialog, this, &MainWindow::OpenServiceSettingsDialog);
|
||||||
QObject::connect(spotify_view_->search_view(), &StreamingSearchView::AddToPlaylist, this, &MainWindow::AddToPlaylist);
|
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
|
#endif
|
||||||
|
|
||||||
QObject::connect(radio_view_, &RadioViewContainer::Refresh, &*app_->radio_services(), &RadioServices::RefreshChannels);
|
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_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_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);
|
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);
|
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_delete_ = playlist_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &MainWindow::PlaylistDelete);
|
||||||
playlist_menu_->addSeparator();
|
playlist_menu_->addSeparator();
|
||||||
playlistitem_actions_separator_ = 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(ui_->playlist, &PlaylistContainer::UndoRedoActionsChanged, this, &MainWindow::PlaylistUndoRedoChanged);
|
||||||
|
|
||||||
QObject::connect(&*app_->device_manager(), &DeviceManager::DeviceError, this, &MainWindow::ShowErrorDialog);
|
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);
|
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);
|
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::ScrobblingEnabledChanged, this, &MainWindow::ScrobblingEnabledChanged);
|
||||||
QObject::connect(&*app_->scrobbler()->settings(), &ScrobblerSettingsService::ScrobbleButtonVisibilityChanged, this, &MainWindow::ScrobbleButtonVisibilityChanged);
|
QObject::connect(&*app_->scrobbler()->settings(), &ScrobblerSettingsService::ScrobbleButtonVisibilityChanged, this, &MainWindow::ScrobbleButtonVisibilityChanged);
|
||||||
@@ -858,14 +851,14 @@ MainWindow::MainWindow(Application *app,
|
|||||||
mac::SetApplicationHandler(this);
|
mac::SetApplicationHandler(this);
|
||||||
#endif
|
#endif
|
||||||
// Tray icon
|
// 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);
|
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(&*tray_icon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
|
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
|
||||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
|
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
|
||||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
|
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
|
||||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
|
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
|
||||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
|
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
|
||||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
|
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
|
||||||
QObject::connect(&*tray_icon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
|
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
|
||||||
|
|
||||||
// Windows 7 thumbbar buttons
|
// Windows 7 thumbbar buttons
|
||||||
#ifdef Q_OS_WIN32
|
#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::UpdateLastPlayed, &*app_->collection_backend(), &CollectionBackend::UpdateLastPlayed);
|
||||||
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdatePlayCount, &*app_->collection_backend(), &CollectionBackend::UpdatePlayCount);
|
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->setEnabled(false);
|
||||||
ui_->action_open_cd->setVisible(false);
|
ui_->action_open_cd->setVisible(false);
|
||||||
#endif
|
#endif
|
||||||
@@ -1043,7 +1036,7 @@ MainWindow::MainWindow(Application *app,
|
|||||||
show();
|
show();
|
||||||
break;
|
break;
|
||||||
case BehaviourSettings::StartupBehaviour::Hide:
|
case BehaviourSettings::StartupBehaviour::Hide:
|
||||||
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible()) {
|
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
[[fallthrough]];
|
[[fallthrough]];
|
||||||
@@ -1056,7 +1049,7 @@ MainWindow::MainWindow(Application *app,
|
|||||||
was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool();
|
was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool();
|
||||||
if (was_minimized_) setWindowState(windowState() | Qt::WindowMinimized);
|
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();
|
show();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1168,13 +1161,13 @@ void MainWindow::ReloadSettings() {
|
|||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
constexpr bool keeprunning_available = true;
|
constexpr bool keeprunning_available = true;
|
||||||
#else
|
#else
|
||||||
const bool systemtray_available = tray_icon_->IsSystemTrayAvailable();
|
const bool systemtray_available = systemtrayicon_->IsSystemTrayAvailable();
|
||||||
s.beginGroup(BehaviourSettings::kSettingsGroup);
|
s.beginGroup(BehaviourSettings::kSettingsGroup);
|
||||||
const bool showtrayicon = s.value(BehaviourSettings::kShowTrayIcon, systemtray_available).toBool();
|
const bool showtrayicon = s.value(BehaviourSettings::kShowTrayIcon, systemtray_available).toBool();
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
const bool keeprunning_available = systemtray_available && showtrayicon;
|
const bool keeprunning_available = systemtray_available && showtrayicon;
|
||||||
if (systemtray_available) {
|
if (systemtray_available) {
|
||||||
tray_icon_->setVisible(showtrayicon);
|
systemtrayicon_->setVisible(showtrayicon);
|
||||||
}
|
}
|
||||||
if ((!showtrayicon || !systemtray_available) && !isVisible()) {
|
if ((!showtrayicon || !systemtray_available) && !isVisible()) {
|
||||||
show();
|
show();
|
||||||
@@ -1199,7 +1192,7 @@ void MainWindow::ReloadSettings() {
|
|||||||
int iconsize = s.value(AppearanceSettings::kIconSizePlayControlButtons, 32).toInt();
|
int iconsize = s.value(AppearanceSettings::kIconSizePlayControlButtons, 32).toInt();
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
tray_icon_->SetTrayiconProgress(trayicon_progress);
|
systemtrayicon_->SetTrayiconProgress(trayicon_progress);
|
||||||
|
|
||||||
#ifdef HAVE_DBUS
|
#ifdef HAVE_DBUS
|
||||||
if (taskbar_progress_ && !taskbar_progress) {
|
if (taskbar_progress_ && !taskbar_progress) {
|
||||||
@@ -1221,11 +1214,11 @@ void MainWindow::ReloadSettings() {
|
|||||||
ui_->volume->SetEnabled(volume_control);
|
ui_->volume->SetEnabled(volume_control);
|
||||||
if (volume_control) {
|
if (volume_control) {
|
||||||
if (!ui_->action_mute->isVisible()) ui_->action_mute->setVisible(true);
|
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 {
|
else {
|
||||||
if (ui_->action_mute->isVisible()) ui_->action_mute->setVisible(false);
|
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) {
|
if (app_->player()->GetState() == EngineBase::State::Playing) {
|
||||||
app_->player()->Stop();
|
app_->player()->Stop();
|
||||||
hide();
|
hide();
|
||||||
if (tray_icon_->IsSystemTrayAvailable()) {
|
if (systemtrayicon_->IsSystemTrayAvailable()) {
|
||||||
tray_icon_->setVisible(false);
|
systemtrayicon_->setVisible(false);
|
||||||
}
|
}
|
||||||
return; // Don't quit the application now: wait for the fadeout finished signal
|
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_->action_love->setEnabled(false);
|
||||||
ui_->button_love->setEnabled(false);
|
ui_->button_love->setEnabled(false);
|
||||||
tray_icon_->LoveStateChanged(false);
|
systemtrayicon_->LoveStateChanged(false);
|
||||||
|
|
||||||
if (track_position_timer_->isActive()) {
|
if (track_position_timer_->isActive()) {
|
||||||
track_position_timer_->stop();
|
track_position_timer_->stop();
|
||||||
@@ -1444,8 +1437,8 @@ void MainWindow::MediaStopped() {
|
|||||||
track_slider_timer_->stop();
|
track_slider_timer_->stop();
|
||||||
}
|
}
|
||||||
ui_->track_slider->SetStopped();
|
ui_->track_slider->SetStopped();
|
||||||
tray_icon_->SetProgress(0);
|
systemtrayicon_->SetProgress(0);
|
||||||
tray_icon_->SetStopped();
|
systemtrayicon_->SetStopped();
|
||||||
|
|
||||||
#ifdef HAVE_DBUS
|
#ifdef HAVE_DBUS
|
||||||
if (taskbar_progress_) {
|
if (taskbar_progress_) {
|
||||||
@@ -1477,7 +1470,7 @@ void MainWindow::MediaPaused() {
|
|||||||
track_slider_timer_->start();
|
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_->action_play_pause->setEnabled(enable_play_pause);
|
||||||
ui_->track_slider->SetCanSeek(can_seek);
|
ui_->track_slider->SetCanSeek(can_seek);
|
||||||
tray_icon_->SetPlaying(enable_play_pause);
|
systemtrayicon_->SetPlaying(enable_play_pause);
|
||||||
|
|
||||||
if (!track_position_timer_->isActive()) {
|
if (!track_position_timer_->isActive()) {
|
||||||
track_position_timer_->start();
|
track_position_timer_->start();
|
||||||
@@ -1515,18 +1508,18 @@ void MainWindow::SendNowPlaying() {
|
|||||||
|
|
||||||
// Send now playing to scrobble services
|
// Send now playing to scrobble services
|
||||||
Playlist *playlist = app_->playlist_manager()->active();
|
Playlist *playlist = app_->playlist_manager()->active();
|
||||||
if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->Metadata().is_metadata_good()) {
|
if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->EffectiveMetadata().is_metadata_good()) {
|
||||||
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->Metadata());
|
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->EffectiveMetadata());
|
||||||
ui_->action_love->setEnabled(true);
|
ui_->action_love->setEnabled(true);
|
||||||
ui_->button_love->setEnabled(true);
|
ui_->button_love->setEnabled(true);
|
||||||
tray_icon_->LoveStateChanged(true);
|
systemtrayicon_->LoveStateChanged(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::VolumeChanged(const uint volume) {
|
void MainWindow::VolumeChanged(const uint volume) {
|
||||||
ui_->action_mute->setChecked(volume == 0);
|
ui_->action_mute->setChecked(volume == 0);
|
||||||
tray_icon_->MuteButtonStateChanged(volume == 0);
|
systemtrayicon_->MuteButtonStateChanged(volume == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::SongChanged(const Song &song) {
|
void MainWindow::SongChanged(const Song &song) {
|
||||||
@@ -1536,7 +1529,7 @@ void MainWindow::SongChanged(const Song &song) {
|
|||||||
song_playing_ = song;
|
song_playing_ = song;
|
||||||
song_ = song;
|
song_ = song;
|
||||||
setWindowTitle(song.PrettyTitleWithArtist());
|
setWindowTitle(song.PrettyTitleWithArtist());
|
||||||
tray_icon_->SetProgress(0);
|
systemtrayicon_->SetProgress(0);
|
||||||
|
|
||||||
#ifdef HAVE_DBUS
|
#ifdef HAVE_DBUS
|
||||||
if (taskbar_progress_) {
|
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 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 position = app_->player()->engine()->position_nanosec();
|
||||||
const qint64 length = app_->player()->engine()->length_nanosec();
|
const qint64 length = app_->player()->engine()->length_nanosec();
|
||||||
const float percentage = (length == 0 ? 1 : static_cast<float>(position) / static_cast<float>(length));
|
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());
|
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) {
|
void MainWindow::hideEvent(QHideEvent *e) {
|
||||||
|
|
||||||
// Some window managers don't remember maximized state between
|
// Some window managers don't remember maximized state between
|
||||||
@@ -1719,7 +1701,7 @@ void MainWindow::hideEvent(QHideEvent *e) {
|
|||||||
|
|
||||||
void MainWindow::closeEvent(QCloseEvent *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();
|
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) {
|
void MainWindow::SetHiddenInTray(const bool hidden) {
|
||||||
|
|
||||||
if (hidden && isVisible()) {
|
if (hidden && isVisible()) {
|
||||||
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible() && keep_running_) {
|
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible() && keep_running_) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -1747,6 +1739,7 @@ void MainWindow::SetHiddenInTray(const bool hidden) {
|
|||||||
else {
|
else {
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
CheckShowErrorDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1758,8 +1751,8 @@ void MainWindow::FilePathChanged(const QString &path) {
|
|||||||
void MainWindow::Seeked(const qint64 microseconds) {
|
void MainWindow::Seeked(const qint64 microseconds) {
|
||||||
|
|
||||||
const qint64 position = microseconds / kUsecPerSec;
|
const qint64 position = microseconds / kUsecPerSec;
|
||||||
const qint64 length = app_->player()->GetCurrentItem()->Metadata().length_nanosec() / kNsecPerSec;
|
const qint64 length = app_->player()->GetCurrentItem()->EffectiveMetadata().length_nanosec() / kNsecPerSec;
|
||||||
tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||||
|
|
||||||
#ifdef HAVE_DBUS
|
#ifdef HAVE_DBUS
|
||||||
if (taskbar_progress_) {
|
if (taskbar_progress_) {
|
||||||
@@ -1774,12 +1767,12 @@ void MainWindow::UpdateTrackPosition() {
|
|||||||
PlaylistItemPtr item(app_->player()->GetCurrentItem());
|
PlaylistItemPtr item(app_->player()->GetCurrentItem());
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
const qint64 length = (item->Metadata().length_nanosec() / kNsecPerSec);
|
const qint64 length = (item->EffectiveMetadata().length_nanosec() / kNsecPerSec);
|
||||||
if (length <= 0) return;
|
if (length <= 0) return;
|
||||||
const int position = std::floor(static_cast<float>(app_->player()->engine()->position_nanosec()) / static_cast<float>(kNsecPerSec) + 0.5);
|
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
|
// 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
|
#ifdef HAVE_DBUS
|
||||||
if (taskbar_progress_) {
|
if (taskbar_progress_) {
|
||||||
@@ -1788,12 +1781,12 @@ void MainWindow::UpdateTrackPosition() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Send Scrobble
|
// 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();
|
Playlist *playlist = app_->playlist_manager()->active();
|
||||||
if (playlist && !playlist->scrobbled()) {
|
if (playlist && !playlist->scrobbled()) {
|
||||||
const qint64 scrobble_point = (playlist->scrobble_point_nanosec() / kNsecPerSec);
|
const qint64 scrobble_point = (playlist->scrobble_point_nanosec() / kNsecPerSec);
|
||||||
if (position >= scrobble_point) {
|
if (position >= scrobble_point) {
|
||||||
app_->scrobbler()->Scrobble(item->Metadata(), scrobble_point);
|
app_->scrobbler()->Scrobble(item->EffectiveMetadata(), scrobble_point);
|
||||||
playlist->set_scrobbled(true);
|
playlist->set_scrobbled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1910,7 +1903,7 @@ void MainWindow::AddToPlaylistFromAction(QAction *action) {
|
|||||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
|
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
items << item;
|
items << item;
|
||||||
songs << item->Metadata();
|
songs << item->EffectiveMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're creating a new playlist
|
// 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());
|
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(src_idx.row());
|
||||||
if (!item) continue;
|
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;
|
cue_selected = true;
|
||||||
}
|
}
|
||||||
else if (item->Metadata().IsEditable()) {
|
else if (item->EffectiveMetadata().IsEditable()) {
|
||||||
++editable;
|
++editable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2032,9 +2025,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
|||||||
playlist_show_in_collection_->setVisible(false);
|
playlist_show_in_collection_->setVisible(false);
|
||||||
playlist_copy_to_collection_->setVisible(false);
|
playlist_copy_to_collection_->setVisible(false);
|
||||||
playlist_move_to_collection_->setVisible(false);
|
playlist_move_to_collection_->setVisible(false);
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
playlist_copy_to_device_->setVisible(false);
|
playlist_copy_to_device_->setVisible(false);
|
||||||
#endif
|
|
||||||
playlist_organize_->setVisible(false);
|
playlist_organize_->setVisible(false);
|
||||||
playlist_delete_->setVisible(false);
|
playlist_delete_->setVisible(false);
|
||||||
|
|
||||||
@@ -2097,7 +2088,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
|||||||
|
|
||||||
// Is it a collection item?
|
// Is it a collection item?
|
||||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
|
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_organize_->setVisible(local_songs > 0 && editable > 0 && !cue_selected);
|
||||||
playlist_show_in_collection_->setVisible(true);
|
playlist_show_in_collection_->setVisible(true);
|
||||||
playlist_open_in_browser_->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);
|
playlist_move_to_collection_->setVisible(local_songs > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
playlist_copy_to_device_->setVisible(local_songs > 0);
|
playlist_copy_to_device_->setVisible(local_songs > 0);
|
||||||
#endif
|
|
||||||
|
|
||||||
playlist_delete_->setVisible(delete_files_ && local_songs > 0);
|
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()));
|
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(source_index.row()));
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
if (item->IsLocalCollectionItem()) {
|
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);
|
QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
|
||||||
app_->playlist_manager()->current()->ItemReload(persistent_index, item->OriginalMetadata(), false);
|
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) {
|
void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
|
||||||
|
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
|
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
|
||||||
organize_dialog_->SetCopy(true);
|
organize_dialog_->SetCopy(true);
|
||||||
if (organize_dialog_->SetUrls(urls)) {
|
if (organize_dialog_->SetUrls(urls)) {
|
||||||
@@ -2761,9 +2749,6 @@ void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
|
|||||||
else {
|
else {
|
||||||
QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device"));
|
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) {
|
for (const QModelIndex &proxy_index : proxy_indexes) {
|
||||||
const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index);
|
const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index);
|
||||||
if (!source_index.isValid()) continue;
|
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);
|
Utilities::OpenInFileBrowser(urls);
|
||||||
@@ -2839,7 +2824,7 @@ void MainWindow::PlaylistCopyUrl() {
|
|||||||
if (!source_index.isValid()) continue;
|
if (!source_index.isValid()) continue;
|
||||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
|
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
urls << item->StreamUrl();
|
urls << item->EffectiveUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urls.count() > 0) {
|
if (urls.count() > 0) {
|
||||||
@@ -2891,8 +2876,6 @@ void MainWindow::PlaylistSkip() {
|
|||||||
|
|
||||||
void MainWindow::PlaylistCopyToDevice() {
|
void MainWindow::PlaylistCopyToDevice() {
|
||||||
|
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
|
|
||||||
SongList songs;
|
SongList songs;
|
||||||
|
|
||||||
const QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
|
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"));
|
QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::ChangeCollectionFilterMode(QAction *action) {
|
void MainWindow::ChangeCollectionFilterMode(QAction *action) {
|
||||||
@@ -3012,6 +2993,14 @@ void MainWindow::ShowErrorDialog(const QString &message) {
|
|||||||
error_dialog_->ShowMessage(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() {
|
void MainWindow::CheckFullRescanRevisions() {
|
||||||
|
|
||||||
int from = app_->database()->startup_schema_version();
|
int from = app_->database()->startup_schema_version();
|
||||||
@@ -3283,7 +3272,7 @@ void MainWindow::LoveButtonVisibilityChanged(const bool value) {
|
|||||||
else
|
else
|
||||||
ui_->widget_love->hide();
|
ui_->widget_love->hide();
|
||||||
|
|
||||||
tray_icon_->LoveVisibilityChanged(value);
|
systemtrayicon_->LoveVisibilityChanged(value);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3306,7 +3295,7 @@ void MainWindow::Love() {
|
|||||||
app_->scrobbler()->Love();
|
app_->scrobbler()->Love();
|
||||||
ui_->button_love->setEnabled(false);
|
ui_->button_love->setEnabled(false);
|
||||||
ui_->action_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) {
|
for (const QModelIndex &proxy_idx : proxy_indexes) {
|
||||||
QModelIndex source_idx = app_->playlist_manager()->current()->filter()->mapToSource(proxy_idx);
|
QModelIndex source_idx = app_->playlist_manager()->current()->filter()->mapToSource(proxy_idx);
|
||||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_idx.row());
|
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_idx.row());
|
||||||
if (!item || !item->Metadata().url().isLocalFile()) continue;
|
if (!item || !item->EffectiveMetadata().url().isLocalFile()) continue;
|
||||||
QString filename = item->Metadata().url().toLocalFile();
|
QString filename = item->EffectiveMetadata().url().toLocalFile();
|
||||||
if (files.contains(filename)) continue;
|
if (files.contains(filename)) continue;
|
||||||
selected_songs << item->Metadata();
|
selected_songs << item->EffectiveMetadata();
|
||||||
files << filename;
|
files << filename;
|
||||||
if (item == app_->player()->GetCurrentItem()) is_current_item = true;
|
if (item == app_->player()->GetCurrentItem()) is_current_item = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -73,9 +73,7 @@ class CollectionViewContainer;
|
|||||||
class CollectionFilter;
|
class CollectionFilter;
|
||||||
class AlbumCoverChoiceController;
|
class AlbumCoverChoiceController;
|
||||||
class CommandlineOptions;
|
class CommandlineOptions;
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
class DeviceViewContainer;
|
class DeviceViewContainer;
|
||||||
#endif
|
|
||||||
class EditTagDialog;
|
class EditTagDialog;
|
||||||
class Equalizer;
|
class Equalizer;
|
||||||
class ErrorDialog;
|
class ErrorDialog;
|
||||||
@@ -113,7 +111,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MainWindow(Application *app,
|
explicit MainWindow(Application *app,
|
||||||
SharedPtr<SystemTrayIcon> tray_icon,
|
SharedPtr<SystemTrayIcon> systemtrayicon,
|
||||||
OSDBase *osd,
|
OSDBase *osd,
|
||||||
#ifdef HAVE_DISCORD_RPC
|
#ifdef HAVE_DISCORD_RPC
|
||||||
discord::RichPresence *discord_rich_presence,
|
discord::RichPresence *discord_rich_presence,
|
||||||
@@ -126,9 +124,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
void CommandlineOptionsReceived(const CommandlineOptions &options);
|
void CommandlineOptionsReceived(const CommandlineOptions &options);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void showEvent(QShowEvent *e) override;
|
|
||||||
void hideEvent(QHideEvent *e) override;
|
void hideEvent(QHideEvent *e) override;
|
||||||
void closeEvent(QCloseEvent *e) override;
|
void closeEvent(QCloseEvent *e) override;
|
||||||
|
void changeEvent(QEvent *e) override;
|
||||||
void keyPressEvent(QKeyEvent *e) override;
|
void keyPressEvent(QKeyEvent *e) override;
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;
|
bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;
|
||||||
@@ -238,6 +236,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
|
|
||||||
void ShowAboutDialog();
|
void ShowAboutDialog();
|
||||||
void ShowErrorDialog(const QString &message);
|
void ShowErrorDialog(const QString &message);
|
||||||
|
void CheckShowErrorDialog();
|
||||||
void ShowTranscodeDialog();
|
void ShowTranscodeDialog();
|
||||||
SettingsDialog *CreateSettingsDialog();
|
SettingsDialog *CreateSettingsDialog();
|
||||||
EditTagDialog *CreateEditTagDialog();
|
EditTagDialog *CreateEditTagDialog();
|
||||||
@@ -312,11 +311,12 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
Application *app_;
|
Application *app_;
|
||||||
SharedPtr<SystemTrayIcon> tray_icon_;
|
SharedPtr<SystemTrayIcon> systemtrayicon_;
|
||||||
OSDBase *osd_;
|
OSDBase *osd_;
|
||||||
#ifdef HAVE_DISCORD_RPC
|
#ifdef HAVE_DISCORD_RPC
|
||||||
discord::RichPresence *discord_rich_presence_;
|
discord::RichPresence *discord_rich_presence_;
|
||||||
#endif
|
#endif
|
||||||
|
Lazy<ErrorDialog> error_dialog_;
|
||||||
Lazy<About> about_dialog_;
|
Lazy<About> about_dialog_;
|
||||||
Lazy<Console> console_;
|
Lazy<Console> console_;
|
||||||
Lazy<EditTagDialog> edit_tag_dialog_;
|
Lazy<EditTagDialog> edit_tag_dialog_;
|
||||||
@@ -327,13 +327,10 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
ContextView *context_view_;
|
ContextView *context_view_;
|
||||||
CollectionViewContainer *collection_view_;
|
CollectionViewContainer *collection_view_;
|
||||||
FileView *file_view_;
|
FileView *file_view_;
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
DeviceViewContainer *device_view_;
|
DeviceViewContainer *device_view_;
|
||||||
#endif
|
|
||||||
PlaylistListContainer *playlist_list_;
|
PlaylistListContainer *playlist_list_;
|
||||||
QueueView *queue_view_;
|
QueueView *queue_view_;
|
||||||
|
|
||||||
Lazy<ErrorDialog> error_dialog_;
|
|
||||||
Lazy<SettingsDialog> settings_dialog_;
|
Lazy<SettingsDialog> settings_dialog_;
|
||||||
Lazy<AlbumCoverManager> cover_manager_;
|
Lazy<AlbumCoverManager> cover_manager_;
|
||||||
SharedPtr<Equalizer> equalizer_;
|
SharedPtr<Equalizer> equalizer_;
|
||||||
@@ -380,9 +377,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
QAction *playlist_move_to_collection_;
|
QAction *playlist_move_to_collection_;
|
||||||
QAction *playlist_open_in_browser_;
|
QAction *playlist_open_in_browser_;
|
||||||
QAction *playlist_organize_;
|
QAction *playlist_organize_;
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
QAction *playlist_copy_to_device_;
|
QAction *playlist_copy_to_device_;
|
||||||
#endif
|
|
||||||
QAction *playlist_delete_;
|
QAction *playlist_delete_;
|
||||||
QAction *playlist_queue_;
|
QAction *playlist_queue_;
|
||||||
QAction *playlist_queue_play_next_;
|
QAction *playlist_queue_play_next_;
|
||||||
|
|||||||
@@ -13,10 +13,6 @@
|
|||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Strawberry Music Player</string>
|
<string>Strawberry Music Player</string>
|
||||||
</property>
|
</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">
|
<widget class="QWidget" name="centralWidget">
|
||||||
<layout class="QVBoxLayout" name="layout_centralWidget">
|
<layout class="QVBoxLayout" name="layout_centralWidget">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
@@ -37,7 +33,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QSplitter" name="splitter">
|
<widget class="QSplitter" name="splitter">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="sidebar_layout">
|
<widget class="QWidget" name="sidebar_layout">
|
||||||
<layout class="QVBoxLayout" name="layout_left">
|
<layout class="QVBoxLayout" name="layout_left">
|
||||||
@@ -77,7 +73,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="Line" name="line_6">
|
<widget class="Line" name="line_6">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -102,7 +98,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QFrame" name="player_controls">
|
<widget class="QFrame" name="player_controls">
|
||||||
<property name="frameShape">
|
<property name="frameShape">
|
||||||
<enum>QFrame::Shape::NoFrame</enum>
|
<enum>QFrame::NoFrame</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QHBoxLayout" name="layout_player_controls">
|
<layout class="QHBoxLayout" name="layout_player_controls">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
@@ -167,7 +163,7 @@
|
|||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="popupMode">
|
<property name="popupMode">
|
||||||
<enum>QToolButton::ToolButtonPopupMode::MenuButtonPopup</enum>
|
<enum>QToolButton::MenuButtonPopup</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="autoRaise">
|
<property name="autoRaise">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
@@ -211,7 +207,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="Line" name="line_love">
|
<widget class="Line" name="line_love">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -237,7 +233,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="Line" name="line_buttons">
|
<widget class="Line" name="line_buttons">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -260,10 +256,10 @@
|
|||||||
<item>
|
<item>
|
||||||
<spacer name="horizontalSpacer">
|
<spacer name="horizontalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeType">
|
<property name="sizeType">
|
||||||
<enum>QSizePolicy::Policy::Expanding</enum>
|
<enum>QSizePolicy::Expanding</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
@@ -276,7 +272,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="Line" name="line_volume">
|
<widget class="Line" name="line_volume">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -292,7 +288,7 @@
|
|||||||
<number>100</number>
|
<number>100</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -326,7 +322,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="Line" name="status_bar_line">
|
<widget class="Line" name="status_bar_line">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -380,7 +376,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="playlist_summary">
|
<widget class="QLabel" name="playlist_summary">
|
||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -391,7 +387,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="Line" name="line_5">
|
<widget class="Line" name="line_5">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -401,7 +397,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="Line" name="line_2">
|
<widget class="Line" name="line_2">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -580,7 +576,7 @@
|
|||||||
<string>Ctrl+Q</string>
|
<string>Ctrl+Q</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="menuRole">
|
<property name="menuRole">
|
||||||
<enum>QAction::MenuRole::QuitRole</enum>
|
<enum>QAction::QuitRole</enum>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_stop_after_this_track">
|
<action name="action_stop_after_this_track">
|
||||||
@@ -644,7 +640,7 @@
|
|||||||
<string>Ctrl+P</string>
|
<string>Ctrl+P</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="menuRole">
|
<property name="menuRole">
|
||||||
<enum>QAction::MenuRole::PreferencesRole</enum>
|
<enum>QAction::PreferencesRole</enum>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_about_strawberry">
|
<action name="action_about_strawberry">
|
||||||
@@ -659,7 +655,7 @@
|
|||||||
<string>F1</string>
|
<string>F1</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="menuRole">
|
<property name="menuRole">
|
||||||
<enum>QAction::MenuRole::AboutRole</enum>
|
<enum>QAction::AboutRole</enum>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_shuffle">
|
<action name="action_shuffle">
|
||||||
@@ -785,7 +781,7 @@
|
|||||||
<string>About &Qt</string>
|
<string>About &Qt</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="menuRole">
|
<property name="menuRole">
|
||||||
<enum>QAction::MenuRole::AboutQtRole</enum>
|
<enum>QAction::AboutQtRole</enum>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_mute">
|
<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;
|
QByteArray user_agent;
|
||||||
if (request.hasRawHeader("User-Agent")) {
|
if (network_request.hasRawHeader("User-Agent")) {
|
||||||
user_agent = request.header(QNetworkRequest::UserAgentHeader).toByteArray();
|
user_agent = network_request.header(QNetworkRequest::UserAgentHeader).toByteArray();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
user_agent = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
|
user_agent = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkRequest new_request(request);
|
QNetworkRequest new_network_request(network_request);
|
||||||
new_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
new_network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
new_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
|
new_network_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
|
||||||
|
|
||||||
if (op == QNetworkAccessManager::PostOperation && !new_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
|
if (op == QNetworkAccessManager::PostOperation && !new_network_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
|
||||||
new_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
|
new_network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer the cache unless the caller has changed the setting already
|
// Prefer the cache unless the caller has changed the setting already
|
||||||
if (request.attribute(QNetworkRequest::CacheLoadControlAttribute).toInt() == QNetworkRequest::PreferNetwork) {
|
if (!network_request.attribute(QNetworkRequest::CacheLoadControlAttribute).isValid()) {
|
||||||
new_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
|
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);
|
explicit NetworkAccessManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
protected:
|
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
|
#endif // NETWORKACCESSMANAGER_H
|
||||||
|
|||||||
@@ -288,10 +288,10 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
|
|||||||
bool is_current = false;
|
bool is_current = false;
|
||||||
bool is_next = false;
|
bool is_next = false;
|
||||||
|
|
||||||
if (result.media_url_ == current_item->Url()) {
|
if (result.media_url_ == current_item->OriginalUrl()) {
|
||||||
is_current = true;
|
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;
|
is_next = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -316,8 +316,8 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
|
|||||||
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
|
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
|
||||||
|
|
||||||
Song song;
|
Song song;
|
||||||
if (is_current) song = current_item->Metadata();
|
if (is_current) song = current_item->EffectiveMetadata();
|
||||||
else if (is_next) song = next_item->Metadata();
|
else if (is_next) song = next_item->EffectiveMetadata();
|
||||||
|
|
||||||
bool update = false;
|
bool update = false;
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
|
|||||||
if (
|
if (
|
||||||
(result.stream_url_.isValid())
|
(result.stream_url_.isValid())
|
||||||
&&
|
&&
|
||||||
(result.stream_url_ != song.url())
|
(result.stream_url_ != song.effective_url())
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
song.set_stream_url(result.stream_url_);
|
song.set_stream_url(result.stream_url_);
|
||||||
@@ -371,14 +371,14 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (is_current) {
|
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());
|
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;
|
current_item_ = current_item;
|
||||||
play_offset_nanosec_ = 0;
|
play_offset_nanosec_ = 0;
|
||||||
}
|
}
|
||||||
else if (is_next && !current_item->Metadata().is_module_music()) {
|
else if (is_next && !current_item->EffectiveMetadata().is_module_music()) {
|
||||||
qLog(Debug) << "Preloading next song" << next_item->Metadata().title() << result.stream_url_;
|
qLog(Debug) << "Preloading next song" << next_item->EffectiveMetadata().title() << result.stream_url_;
|
||||||
engine_->StartPreloading(next_item->Url(), result.stream_url_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec());
|
engine_->StartPreloading(next_item->OriginalUrl(), result.stream_url_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec());
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -504,8 +504,8 @@ bool Player::HandleStopAfter(const Playlist::AutoScroll autoscroll) {
|
|||||||
|
|
||||||
void Player::TrackEnded() {
|
void Player::TrackEnded() {
|
||||||
|
|
||||||
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->Metadata().id() != -1) {
|
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->EffectiveMetadata().id() != -1) {
|
||||||
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->Metadata().id());
|
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->EffectiveMetadata().id());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HandleStopAfter(Playlist::AutoScroll::Maybe)) return;
|
if (HandleStopAfter(Playlist::AutoScroll::Maybe)) return;
|
||||||
@@ -554,7 +554,7 @@ void Player::PlayPause(const quint64 offset_nanosec, const Playlist::AutoScroll
|
|||||||
void Player::UnPause() {
|
void Player::UnPause() {
|
||||||
|
|
||||||
if (current_item_ && pause_time_.isValid()) {
|
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()) {
|
if (url_handlers_->CanHandle(song.url()) && song.stream_url_can_expire()) {
|
||||||
const qint64 time = QDateTime::currentSecsSinceEpoch() - pause_time_.toSecsSinceEpoch();
|
const qint64 time = QDateTime::currentSecsSinceEpoch() - pause_time_.toSecsSinceEpoch();
|
||||||
if (time >= 30) { // Stream URL might be expired.
|
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_);
|
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;
|
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();
|
current_item_ = playlist_manager_->active()->current_item();
|
||||||
const QUrl url = current_item_->StreamUrl();
|
const QUrl url = current_item_->EffectiveUrl();
|
||||||
|
|
||||||
if (url_handlers_->CanHandle(url)) {
|
if (url_handlers_->CanHandle(url)) {
|
||||||
// It's already loading
|
// 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));
|
HandleLoadResult(url_handler->StartLoading(url));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
qLog(Debug) << "Playing song" << current_item_->Metadata().title() << url << "position" << offset_nanosec;
|
qLog(Debug) << "Playing song" << current_item_->EffectiveMetadata().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());
|
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();
|
const int current_row = playlist_manager_->active()->current_row();
|
||||||
if (current_row != -1) {
|
if (current_row != -1) {
|
||||||
PlaylistItemPtr item = playlist_manager_->active()->current_item();
|
PlaylistItemPtr item = playlist_manager_->active()->current_item();
|
||||||
if (item && engine_metadata.media_url == item->Url()) {
|
if (item && engine_metadata.media_url == item->OriginalUrl()) {
|
||||||
Song song = item->Metadata();
|
Song song = item->EffectiveMetadata();
|
||||||
song.MergeFromEngineMetadata(engine_metadata);
|
song.MergeFromEngineMetadata(engine_metadata);
|
||||||
playlist_manager_->active()->UpdateItemMetadata(current_row, item, song, true);
|
playlist_manager_->active()->UpdateItemMetadata(current_row, item, song, true);
|
||||||
return;
|
return;
|
||||||
@@ -836,8 +836,8 @@ void Player::EngineMetadataReceived(const EngineMetadata &engine_metadata) {
|
|||||||
const int next_row = playlist_manager_->active()->next_row();
|
const int next_row = playlist_manager_->active()->next_row();
|
||||||
if (next_row != -1) {
|
if (next_row != -1) {
|
||||||
PlaylistItemPtr next_item = playlist_manager_->active()->item_at(next_row);
|
PlaylistItemPtr next_item = playlist_manager_->active()->item_at(next_row);
|
||||||
if (engine_metadata.media_url == next_item->Url()) {
|
if (engine_metadata.media_url == next_item->OriginalUrl()) {
|
||||||
Song song = next_item->Metadata();
|
Song song = next_item->EffectiveMetadata();
|
||||||
song.MergeFromEngineMetadata(engine_metadata);
|
song.MergeFromEngineMetadata(engine_metadata);
|
||||||
playlist_manager_->active()->UpdateItemMetadata(next_row, next_item, song, true);
|
playlist_manager_->active()->UpdateItemMetadata(next_row, next_item, song, true);
|
||||||
}
|
}
|
||||||
@@ -905,11 +905,11 @@ void Player::PlayWithPause(const quint64 offset_nanosec) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Player::ShowOSD() {
|
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() {
|
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() {
|
void Player::TrackAboutToEnd() {
|
||||||
@@ -932,7 +932,7 @@ void Player::TrackAboutToEnd() {
|
|||||||
|
|
||||||
// If the next track is on the same album (or same cue file),
|
// 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.
|
// 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();
|
TrackEnded();
|
||||||
return;
|
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.
|
// 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;
|
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.
|
// Get the actual track URL rather than the stream URL.
|
||||||
if (url_handlers_->CanHandle(url)) {
|
if (url_handlers_->CanHandle(url)) {
|
||||||
@@ -961,20 +961,20 @@ void Player::TrackAboutToEnd() {
|
|||||||
case UrlHandler::LoadResult::Type::TrackAvailable:
|
case UrlHandler::LoadResult::Type::TrackAvailable:
|
||||||
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
|
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
|
||||||
url = result.stream_url_;
|
url = result.stream_url_;
|
||||||
Song song = next_item->Metadata();
|
Song song = next_item->EffectiveMetadata();
|
||||||
song.set_stream_url(url);
|
song.set_stream_url(url);
|
||||||
next_item->SetTemporaryMetadata(song);
|
next_item->SetStreamMetadata(song);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preloading any format while currently playing module music is broken in GStreamer.
|
// Preloading any format while currently playing module music is broken in GStreamer.
|
||||||
// See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/769
|
// 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;
|
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
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -68,9 +68,13 @@
|
|||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
const QStringList Song::kColumns = QStringList() << u"title"_s
|
const QStringList Song::kColumns = QStringList() << u"title"_s
|
||||||
|
<< u"titlesort"_s
|
||||||
<< u"album"_s
|
<< u"album"_s
|
||||||
|
<< u"albumsort"_s
|
||||||
<< u"artist"_s
|
<< u"artist"_s
|
||||||
|
<< u"artistsort"_s
|
||||||
<< u"albumartist"_s
|
<< u"albumartist"_s
|
||||||
|
<< u"albumartistsort"_s
|
||||||
<< u"track"_s
|
<< u"track"_s
|
||||||
<< u"disc"_s
|
<< u"disc"_s
|
||||||
<< u"year"_s
|
<< u"year"_s
|
||||||
@@ -78,7 +82,9 @@ const QStringList Song::kColumns = QStringList() << u"title"_s
|
|||||||
<< u"genre"_s
|
<< u"genre"_s
|
||||||
<< u"compilation"_s
|
<< u"compilation"_s
|
||||||
<< u"composer"_s
|
<< u"composer"_s
|
||||||
|
<< u"composersort"_s
|
||||||
<< u"performer"_s
|
<< u"performer"_s
|
||||||
|
<< u"performersort"_s
|
||||||
<< u"grouping"_s
|
<< u"grouping"_s
|
||||||
<< u"comment"_s
|
<< u"comment"_s
|
||||||
<< u"lyrics"_s
|
<< u"lyrics"_s
|
||||||
@@ -126,6 +132,9 @@ const QStringList Song::kColumns = QStringList() << u"title"_s
|
|||||||
<< u"cue_path"_s
|
<< u"cue_path"_s
|
||||||
|
|
||||||
<< u"rating"_s
|
<< u"rating"_s
|
||||||
|
<< u"bpm"_s
|
||||||
|
<< u"mood"_s
|
||||||
|
<< u"initial_key"_s
|
||||||
|
|
||||||
<< u"acoustid_id"_s
|
<< u"acoustid_id"_s
|
||||||
<< u"acoustid_fingerprint"_s
|
<< u"acoustid_fingerprint"_s
|
||||||
@@ -234,6 +243,7 @@ const QStringList Song::kAcceptedExtensions = QStringList() << u"wav"_s
|
|||||||
<< u"tta"_s
|
<< u"tta"_s
|
||||||
<< u"dsf"_s
|
<< u"dsf"_s
|
||||||
<< u"dsd"_s
|
<< u"dsd"_s
|
||||||
|
<< u"webm"_s
|
||||||
<< u"ac3"_s
|
<< u"ac3"_s
|
||||||
<< u"dts"_s
|
<< u"dts"_s
|
||||||
<< u"spc"_s
|
<< u"spc"_s
|
||||||
@@ -261,9 +271,13 @@ struct Song::Private : public QSharedData {
|
|||||||
bool valid_;
|
bool valid_;
|
||||||
|
|
||||||
QString title_;
|
QString title_;
|
||||||
|
QString titlesort_;
|
||||||
QString album_;
|
QString album_;
|
||||||
|
QString albumsort_;
|
||||||
QString artist_;
|
QString artist_;
|
||||||
|
QString artistsort_;
|
||||||
QString albumartist_;
|
QString albumartist_;
|
||||||
|
QString albumartistsort_;
|
||||||
int track_;
|
int track_;
|
||||||
int disc_;
|
int disc_;
|
||||||
int year_;
|
int year_;
|
||||||
@@ -271,7 +285,9 @@ struct Song::Private : public QSharedData {
|
|||||||
QString genre_;
|
QString genre_;
|
||||||
bool compilation_; // From the file tag
|
bool compilation_; // From the file tag
|
||||||
QString composer_;
|
QString composer_;
|
||||||
|
QString composersort_;
|
||||||
QString performer_;
|
QString performer_;
|
||||||
|
QString performersort_;
|
||||||
QString grouping_;
|
QString grouping_;
|
||||||
QString comment_;
|
QString comment_;
|
||||||
QString lyrics_;
|
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.
|
QString cue_path_; // If the song has a CUE, this contains it's path.
|
||||||
|
|
||||||
float rating_; // Database rating, initial rating read from tag.
|
float rating_; // Database rating, initial rating read from tag.
|
||||||
|
float bpm_;
|
||||||
|
QString mood_;
|
||||||
|
QString initial_key_;
|
||||||
|
|
||||||
QString acoustid_id_;
|
QString acoustid_id_;
|
||||||
QString acoustid_fingerprint_;
|
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 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.
|
bool suspicious_tags_; // Whether our encoding guesser thinks these tags might be incorrectly encoded.
|
||||||
|
|
||||||
QString title_sortable_;
|
QUrl stream_url_; // Temporary stream URL set by the URL handler.
|
||||||
QString album_sortable_;
|
|
||||||
QString artist_sortable_;
|
|
||||||
QString albumartist_sortable_;
|
|
||||||
|
|
||||||
QUrl stream_url_; // Temporary stream url set by the URL handler.
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -384,6 +398,7 @@ Song::Private::Private(const Source source)
|
|||||||
art_unset_(false),
|
art_unset_(false),
|
||||||
|
|
||||||
rating_(-1),
|
rating_(-1),
|
||||||
|
bpm_(-1),
|
||||||
|
|
||||||
init_from_file_(false),
|
init_from_file_(false),
|
||||||
suspicious_tags_(false)
|
suspicious_tags_(false)
|
||||||
@@ -411,9 +426,13 @@ int Song::id() const { return d->id_; }
|
|||||||
bool Song::is_valid() const { return d->valid_; }
|
bool Song::is_valid() const { return d->valid_; }
|
||||||
|
|
||||||
const QString &Song::title() const { return d->title_; }
|
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::album() const { return d->album_; }
|
||||||
|
const QString &Song::albumsort() const { return d->albumsort_; }
|
||||||
const QString &Song::artist() const { return d->artist_; }
|
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::albumartist() const { return d->albumartist_; }
|
||||||
|
const QString &Song::albumartistsort() const { return d->albumartistsort_; }
|
||||||
int Song::track() const { return d->track_; }
|
int Song::track() const { return d->track_; }
|
||||||
int Song::disc() const { return d->disc_; }
|
int Song::disc() const { return d->disc_; }
|
||||||
int Song::year() const { return d->year_; }
|
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_; }
|
const QString &Song::genre() const { return d->genre_; }
|
||||||
bool Song::compilation() const { return d->compilation_; }
|
bool Song::compilation() const { return d->compilation_; }
|
||||||
const QString &Song::composer() const { return d->composer_; }
|
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::performer() const { return d->performer_; }
|
||||||
|
const QString &Song::performersort() const { return d->performersort_; }
|
||||||
const QString &Song::grouping() const { return d->grouping_; }
|
const QString &Song::grouping() const { return d->grouping_; }
|
||||||
const QString &Song::comment() const { return d->comment_; }
|
const QString &Song::comment() const { return d->comment_; }
|
||||||
const QString &Song::lyrics() const { return d->lyrics_; }
|
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_; }
|
const QString &Song::cue_path() const { return d->cue_path_; }
|
||||||
|
|
||||||
float Song::rating() const { return d->rating_; }
|
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_id() const { return d->acoustid_id_; }
|
||||||
const QString &Song::acoustid_fingerprint() const { return d->acoustid_fingerprint_; }
|
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_; }
|
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_; }
|
const QUrl &Song::stream_url() const { return d->stream_url_; }
|
||||||
|
|
||||||
void Song::set_id(const int id) { d->id_ = id; }
|
void Song::set_id(const int id) { d->id_ = id; }
|
||||||
void Song::set_valid(const bool v) { d->valid_ = v; }
|
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_title(const QString &v) { d->title_ = v; }
|
||||||
void Song::set_album(const QString &v) { d->album_sortable_ = sortable(v); d->album_ = v; }
|
void Song::set_titlesort(const QString &v) { d->titlesort_ = v; }
|
||||||
void Song::set_artist(const QString &v) { d->artist_sortable_ = sortable(v); d->artist_ = v; }
|
void Song::set_album(const QString &v) { d->album_ = v; }
|
||||||
void Song::set_albumartist(const QString &v) { d->albumartist_sortable_ = sortable(v); d->albumartist_ = 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_track(const int v) { d->track_ = v; }
|
||||||
void Song::set_disc(const int v) { d->disc_ = v; }
|
void Song::set_disc(const int v) { d->disc_ = v; }
|
||||||
void Song::set_year(const int v) { d->year_ = 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_genre(const QString &v) { d->genre_ = v; }
|
||||||
void Song::set_compilation(const bool v) { d->compilation_ = v; }
|
void Song::set_compilation(const bool v) { d->compilation_ = v; }
|
||||||
void Song::set_composer(const QString &v) { d->composer_ = 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_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_grouping(const QString &v) { d->grouping_ = v; }
|
||||||
void Song::set_comment(const QString &v) { d->comment_ = v; }
|
void Song::set_comment(const QString &v) { d->comment_ = v; }
|
||||||
void Song::set_lyrics(const QString &v) { d->lyrics_ = 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_cue_path(const QString &v) { d->cue_path_ = v; }
|
||||||
|
|
||||||
void Song::set_rating(const float v) { d->rating_ = 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_id(const QString &v) { d->acoustid_id_ = v; }
|
||||||
void Song::set_acoustid_fingerprint(const QString &v) { d->acoustid_fingerprint_ = 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_stream_url(const QUrl &v) { d->stream_url_ = v; }
|
||||||
|
|
||||||
void Song::set_title(const TagLib::String &v) {
|
void Song::set_title(const TagLib::String &v) { d->title_ = TagLibStringToQString(v); }
|
||||||
|
void Song::set_titlesort(const TagLib::String &v) { d->titlesort_ = TagLibStringToQString(v); }
|
||||||
const QString title = TagLibStringToQString(v);
|
void Song::set_album(const TagLib::String &v) { d->album_ = TagLibStringToQString(v); }
|
||||||
d->title_sortable_ = sortable(title);
|
void Song::set_albumsort(const TagLib::String &v) { d->albumsort_ = TagLibStringToQString(v); }
|
||||||
d->title_ = title;
|
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_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_genre(const TagLib::String &v) { d->genre_ = 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_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_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_grouping(const TagLib::String &v) { d->grouping_ = TagLibStringToQString(v); }
|
||||||
void Song::set_comment(const TagLib::String &v) { d->comment_ = 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); }
|
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_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_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_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() 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_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_; }
|
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_effective_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_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_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
|
||||||
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
|
bool Song::is_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;
|
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) {
|
bool Song::save_embedded_cover_supported(const FileType filetype) {
|
||||||
|
|
||||||
return filetype == FileType::FLAC ||
|
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) {
|
int Song::ColumnIndex(const QString &field) {
|
||||||
|
|
||||||
return static_cast<int>(kRowIdColumns.indexOf(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);
|
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 {
|
bool Song::IsMetadataEqual(const Song &other) const {
|
||||||
|
|
||||||
return d->title_ == other.d->title_ &&
|
return d->title_ == other.d->title_ &&
|
||||||
d->album_ == other.d->album_ &&
|
d->titlesort_ == other.d->titlesort_ &&
|
||||||
d->artist_ == other.d->artist_ &&
|
d->album_ == other.d->album_ &&
|
||||||
d->albumartist_ == other.d->albumartist_ &&
|
d->albumsort_ == other.d->albumsort_ &&
|
||||||
d->track_ == other.d->track_ &&
|
d->artist_ == other.d->artist_ &&
|
||||||
d->disc_ == other.d->disc_ &&
|
d->artistsort_ == other.d->artistsort_ &&
|
||||||
d->year_ == other.d->year_ &&
|
d->albumartist_ == other.d->albumartist_ &&
|
||||||
d->originalyear_ == other.d->originalyear_ &&
|
d->albumartistsort_ == other.d->albumartistsort_ &&
|
||||||
d->genre_ == other.d->genre_ &&
|
d->track_ == other.d->track_ &&
|
||||||
d->compilation_ == other.d->compilation_ &&
|
d->disc_ == other.d->disc_ &&
|
||||||
d->composer_ == other.d->composer_ &&
|
d->year_ == other.d->year_ &&
|
||||||
d->performer_ == other.d->performer_ &&
|
d->originalyear_ == other.d->originalyear_ &&
|
||||||
d->grouping_ == other.d->grouping_ &&
|
d->genre_ == other.d->genre_ &&
|
||||||
d->comment_ == other.d->comment_ &&
|
d->compilation_ == other.d->compilation_ &&
|
||||||
d->lyrics_ == other.d->lyrics_ &&
|
d->composer_ == other.d->composer_ &&
|
||||||
d->artist_id_ == other.d->artist_id_ &&
|
d->composersort_ == other.d->composersort_ &&
|
||||||
d->album_id_ == other.d->album_id_ &&
|
d->performer_ == other.d->performer_ &&
|
||||||
d->song_id_ == other.d->song_id_ &&
|
d->performersort_ == other.d->performersort_ &&
|
||||||
d->beginning_ == other.d->beginning_ &&
|
d->grouping_ == other.d->grouping_ &&
|
||||||
length_nanosec() == other.length_nanosec() &&
|
d->comment_ == other.d->comment_ &&
|
||||||
d->bitrate_ == other.d->bitrate_ &&
|
d->lyrics_ == other.d->lyrics_ &&
|
||||||
d->samplerate_ == other.d->samplerate_ &&
|
d->artist_id_ == other.d->artist_id_ &&
|
||||||
d->bitdepth_ == other.d->bitdepth_ &&
|
d->album_id_ == other.d->album_id_ &&
|
||||||
d->cue_path_ == other.d->cue_path_;
|
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 {
|
bool Song::IsPlayStatisticsEqual(const Song &other) const {
|
||||||
|
|
||||||
return d->playcount_ == other.d->playcount_ &&
|
return d->playcount_ == other.d->playcount_ &&
|
||||||
d->skipcount_ == other.d->skipcount_ &&
|
d->skipcount_ == other.d->skipcount_ &&
|
||||||
d->lastplayed_ == other.d->lastplayed_;
|
d->lastplayed_ == other.d->lastplayed_;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -980,42 +1028,70 @@ bool Song::IsAcoustIdEqual(const Song &other) const {
|
|||||||
bool Song::IsMusicBrainzEqual(const Song &other) const {
|
bool Song::IsMusicBrainzEqual(const Song &other) const {
|
||||||
|
|
||||||
return d->musicbrainz_album_artist_id_ == other.d->musicbrainz_album_artist_id_ &&
|
return d->musicbrainz_album_artist_id_ == other.d->musicbrainz_album_artist_id_ &&
|
||||||
d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ &&
|
d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ &&
|
||||||
d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ &&
|
d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ &&
|
||||||
d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ &&
|
d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ &&
|
||||||
d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ &&
|
d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ &&
|
||||||
d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ &&
|
d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ &&
|
||||||
d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ &&
|
d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ &&
|
||||||
d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ &&
|
d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ &&
|
||||||
d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ &&
|
d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ &&
|
||||||
d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_;
|
d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Song::IsEBUR128Equal(const Song &other) const {
|
bool Song::IsEBUR128Equal(const Song &other) const {
|
||||||
|
|
||||||
return d->ebur128_integrated_loudness_lufs_ == other.d->ebur128_integrated_loudness_lufs_ &&
|
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 {
|
bool Song::IsArtEqual(const Song &other) const {
|
||||||
|
|
||||||
return d->art_embedded_ == other.d->art_embedded_ &&
|
return d->art_embedded_ == other.d->art_embedded_ &&
|
||||||
d->art_automatic_ == other.d->art_automatic_ &&
|
d->art_automatic_ == other.d->art_automatic_ &&
|
||||||
d->art_manual_ == other.d->art_manual_ &&
|
d->art_manual_ == other.d->art_manual_ &&
|
||||||
d->art_unset_ == other.d->art_unset_;
|
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 {
|
bool Song::IsAllMetadataEqual(const Song &other) const {
|
||||||
|
|
||||||
return IsMetadataEqual(other) &&
|
return IsMetadataEqual(other) &&
|
||||||
IsPlayStatisticsEqual(other) &&
|
IsPlayStatisticsEqual(other) &&
|
||||||
IsRatingEqual(other) &&
|
IsRatingEqual(other) &&
|
||||||
IsAcoustIdEqual(other) &&
|
IsAcoustIdEqual(other) &&
|
||||||
IsMusicBrainzEqual(other) &&
|
IsMusicBrainzEqual(other) &&
|
||||||
IsArtEqual(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) {
|
QString Song::TextForFiletype(const FileType filetype) {
|
||||||
|
|
||||||
switch (filetype) {
|
switch (filetype) {
|
||||||
@@ -1166,6 +1258,7 @@ QString Song::TextForFiletype(const FileType filetype) {
|
|||||||
case FileType::CDDA: return u"CDDA"_s;
|
case FileType::CDDA: return u"CDDA"_s;
|
||||||
case FileType::SPC: return u"SNES SPC700"_s;
|
case FileType::SPC: return u"SNES SPC700"_s;
|
||||||
case FileType::VGM: return u"VGM"_s;
|
case FileType::VGM: return u"VGM"_s;
|
||||||
|
case FileType::ALAC: return u"ALAC"_s;
|
||||||
case FileType::Stream: return u"Stream"_s;
|
case FileType::Stream: return u"Stream"_s;
|
||||||
case FileType::Unknown:
|
case FileType::Unknown:
|
||||||
default: return QObject::tr("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::IT: return u"it"_s;
|
||||||
case FileType::SPC: return u"spc"_s;
|
case FileType::SPC: return u"spc"_s;
|
||||||
case FileType::VGM: return u"vgm"_s;
|
case FileType::VGM: return u"vgm"_s;
|
||||||
|
case FileType::ALAC: return u"m4a"_s;
|
||||||
case FileType::Unknown:
|
case FileType::Unknown:
|
||||||
default: return u"dat"_s;
|
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::IT: return IconLoader::Load(u"it"_s);
|
||||||
case FileType::CDDA: return IconLoader::Load(u"cd"_s);
|
case FileType::CDDA: return IconLoader::Load(u"cd"_s);
|
||||||
case FileType::Stream: return IconLoader::Load(u"applications-internet"_s);
|
case FileType::Stream: return IconLoader::Load(u"applications-internet"_s);
|
||||||
|
case FileType::ALAC: return IconLoader::Load(u"alac"_s);
|
||||||
case FileType::Unknown:
|
case FileType::Unknown:
|
||||||
default: return IconLoader::Load(u"edit-delete"_s);
|
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 {
|
bool Song::IsFileLossless() const {
|
||||||
|
|
||||||
switch (filetype()) {
|
switch (filetype()) {
|
||||||
@@ -1250,6 +1362,7 @@ bool Song::IsFileLossless() const {
|
|||||||
case FileType::TrueAudio:
|
case FileType::TrueAudio:
|
||||||
case FileType::PCM:
|
case FileType::PCM:
|
||||||
case FileType::CDDA:
|
case FileType::CDDA:
|
||||||
|
case FileType::ALAC:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
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-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-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-vgm"_L1, Qt::CaseInsensitive) == 0) return FileType::VGM;
|
||||||
|
if (mimetype.compare("audio/x-alac"_L1, Qt::CaseInsensitive) == 0) return FileType::ALAC;
|
||||||
|
|
||||||
return FileType::Unknown;
|
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("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("SNES SPC700"_L1, Qt::CaseInsensitive) == 0) return FileType::SPC;
|
||||||
if (text.compare("VGM"_L1, Qt::CaseInsensitive) == 0) return FileType::VGM;
|
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;
|
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);
|
d->id_ = SqlHelper::ValueToInt(r, ColumnIndex(u"ROWID"_s) + col);
|
||||||
|
|
||||||
set_title(SqlHelper::ValueToString(r, ColumnIndex(u"title"_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_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_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_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->track_ = SqlHelper::ValueToInt(r, ColumnIndex(u"track"_s) + col);
|
||||||
d->disc_ = SqlHelper::ValueToInt(r, ColumnIndex(u"disc"_s) + col);
|
d->disc_ = SqlHelper::ValueToInt(r, ColumnIndex(u"disc"_s) + col);
|
||||||
d->year_ = SqlHelper::ValueToInt(r, ColumnIndex(u"year"_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->genre_ = SqlHelper::ValueToString(r, ColumnIndex(u"genre"_s) + col);
|
||||||
d->compilation_ = r.value(ColumnIndex(u"compilation"_s) + col).toBool();
|
d->compilation_ = r.value(ColumnIndex(u"compilation"_s) + col).toBool();
|
||||||
d->composer_ = SqlHelper::ValueToString(r, ColumnIndex(u"composer"_s) + col);
|
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->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->grouping_ = SqlHelper::ValueToString(r, ColumnIndex(u"grouping"_s) + col);
|
||||||
d->comment_ = SqlHelper::ValueToString(r, ColumnIndex(u"comment"_s) + col);
|
d->comment_ = SqlHelper::ValueToString(r, ColumnIndex(u"comment"_s) + col);
|
||||||
d->lyrics_ = SqlHelper::ValueToString(r, ColumnIndex(u"lyrics"_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->art_unset_ = SqlHelper::ValueToBool(r, ColumnIndex(u"art_unset"_s) + col);
|
||||||
|
|
||||||
d->cue_path_ = SqlHelper::ValueToString(r, ColumnIndex(u"cue_path"_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->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_id_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_id"_s) + col);
|
||||||
d->acoustid_fingerprint_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_fingerprint"_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
|
// Remember to bind these in the same order as kBindSpec
|
||||||
|
|
||||||
query->BindStringValue(u":title"_s, d->title_);
|
query->BindStringValue(u":title"_s, d->title_);
|
||||||
|
query->BindStringValue(u":titlesort"_s, d->titlesort_);
|
||||||
query->BindStringValue(u":album"_s, d->album_);
|
query->BindStringValue(u":album"_s, d->album_);
|
||||||
|
query->BindStringValue(u":albumsort"_s, d->albumsort_);
|
||||||
query->BindStringValue(u":artist"_s, d->artist_);
|
query->BindStringValue(u":artist"_s, d->artist_);
|
||||||
|
query->BindStringValue(u":artistsort"_s, d->artistsort_);
|
||||||
query->BindStringValue(u":albumartist"_s, d->albumartist_);
|
query->BindStringValue(u":albumartist"_s, d->albumartist_);
|
||||||
|
query->BindStringValue(u":albumartistsort"_s, d->albumartistsort_);
|
||||||
query->BindIntValue(u":track"_s, d->track_);
|
query->BindIntValue(u":track"_s, d->track_);
|
||||||
query->BindIntValue(u":disc"_s, d->disc_);
|
query->BindIntValue(u":disc"_s, d->disc_);
|
||||||
query->BindIntValue(u":year"_s, d->year_);
|
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->BindStringValue(u":genre"_s, d->genre_);
|
||||||
query->BindBoolValue(u":compilation"_s, d->compilation_);
|
query->BindBoolValue(u":compilation"_s, d->compilation_);
|
||||||
query->BindStringValue(u":composer"_s, d->composer_);
|
query->BindStringValue(u":composer"_s, d->composer_);
|
||||||
|
query->BindStringValue(u":composersort"_s, d->composersort_);
|
||||||
query->BindStringValue(u":performer"_s, d->performer_);
|
query->BindStringValue(u":performer"_s, d->performer_);
|
||||||
|
query->BindStringValue(u":performersort"_s, d->performersort_);
|
||||||
query->BindStringValue(u":grouping"_s, d->grouping_);
|
query->BindStringValue(u":grouping"_s, d->grouping_);
|
||||||
query->BindStringValue(u":comment"_s, d->comment_);
|
query->BindStringValue(u":comment"_s, d->comment_);
|
||||||
query->BindStringValue(u":lyrics"_s, d->lyrics_);
|
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->BindValue(u":cue_path"_s, d->cue_path_);
|
||||||
|
|
||||||
query->BindFloatValue(u":rating"_s, d->rating_);
|
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_id"_s, d->acoustid_id_);
|
||||||
query->BindStringValue(u":acoustid_fingerprint"_s, d->acoustid_fingerprint_);
|
query->BindStringValue(u":acoustid_fingerprint"_s, d->acoustid_fingerprint_);
|
||||||
@@ -1819,7 +1953,7 @@ void Song::ToXesam(QVariantMap *map) const {
|
|||||||
using mpris::AddMetadataAsList;
|
using mpris::AddMetadataAsList;
|
||||||
using mpris::AsMPRISDateTimeType;
|
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);
|
AddMetadata(u"xesam:title"_s, PrettyTitle(), map);
|
||||||
AddMetadataAsList(u"xesam:artist"_s, artist(), map);
|
AddMetadataAsList(u"xesam:artist"_s, artist(), map);
|
||||||
AddMetadata(u"xesam:album"_s, album(), map);
|
AddMetadata(u"xesam:album"_s, album(), map);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -105,6 +105,7 @@ class Song {
|
|||||||
IT = 21,
|
IT = 21,
|
||||||
SPC = 22,
|
SPC = 22,
|
||||||
VGM = 23,
|
VGM = 23,
|
||||||
|
ALAC = 24, // MP4, with ALAC codec
|
||||||
CDDA = 90,
|
CDDA = 90,
|
||||||
Stream = 91
|
Stream = 91
|
||||||
};
|
};
|
||||||
@@ -149,9 +150,13 @@ class Song {
|
|||||||
bool is_valid() const;
|
bool is_valid() const;
|
||||||
|
|
||||||
const QString &title() const;
|
const QString &title() const;
|
||||||
|
const QString &titlesort() const;
|
||||||
const QString &album() const;
|
const QString &album() const;
|
||||||
|
const QString &albumsort() const;
|
||||||
const QString &artist() const;
|
const QString &artist() const;
|
||||||
|
const QString &artistsort() const;
|
||||||
const QString &albumartist() const;
|
const QString &albumartist() const;
|
||||||
|
const QString &albumartistsort() const;
|
||||||
int track() const;
|
int track() const;
|
||||||
int disc() const;
|
int disc() const;
|
||||||
int year() const;
|
int year() const;
|
||||||
@@ -159,7 +164,9 @@ class Song {
|
|||||||
const QString &genre() const;
|
const QString &genre() const;
|
||||||
bool compilation() const;
|
bool compilation() const;
|
||||||
const QString &composer() const;
|
const QString &composer() const;
|
||||||
|
const QString &composersort() const;
|
||||||
const QString &performer() const;
|
const QString &performer() const;
|
||||||
|
const QString &performersort() const;
|
||||||
const QString &grouping() const;
|
const QString &grouping() const;
|
||||||
const QString &comment() const;
|
const QString &comment() const;
|
||||||
const QString &lyrics() const;
|
const QString &lyrics() const;
|
||||||
@@ -206,6 +213,9 @@ class Song {
|
|||||||
const QString &cue_path() const;
|
const QString &cue_path() const;
|
||||||
|
|
||||||
float rating() const;
|
float rating() const;
|
||||||
|
float bpm() const;
|
||||||
|
const QString &mood() const;
|
||||||
|
const QString &initial_key() const;
|
||||||
|
|
||||||
const QString &acoustid_id() const;
|
const QString &acoustid_id() const;
|
||||||
const QString &acoustid_fingerprint() const;
|
const QString &acoustid_fingerprint() const;
|
||||||
@@ -249,11 +259,6 @@ class Song {
|
|||||||
|
|
||||||
bool init_from_file() const;
|
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;
|
const QUrl &stream_url() const;
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
@@ -261,9 +266,13 @@ class Song {
|
|||||||
void set_valid(const bool v);
|
void set_valid(const bool v);
|
||||||
|
|
||||||
void set_title(const QString &v);
|
void set_title(const QString &v);
|
||||||
|
void set_titlesort(const QString &v);
|
||||||
void set_album(const QString &v);
|
void set_album(const QString &v);
|
||||||
|
void set_albumsort(const QString &v);
|
||||||
void set_artist(const QString &v);
|
void set_artist(const QString &v);
|
||||||
|
void set_artistsort(const QString &v);
|
||||||
void set_albumartist(const QString &v);
|
void set_albumartist(const QString &v);
|
||||||
|
void set_albumartistsort(const QString &v);
|
||||||
void set_track(const int v);
|
void set_track(const int v);
|
||||||
void set_disc(const int v);
|
void set_disc(const int v);
|
||||||
void set_year(const int v);
|
void set_year(const int v);
|
||||||
@@ -271,7 +280,9 @@ class Song {
|
|||||||
void set_genre(const QString &v);
|
void set_genre(const QString &v);
|
||||||
void set_compilation(bool v);
|
void set_compilation(bool v);
|
||||||
void set_composer(const QString &v);
|
void set_composer(const QString &v);
|
||||||
|
void set_composersort(const QString &v);
|
||||||
void set_performer(const QString &v);
|
void set_performer(const QString &v);
|
||||||
|
void set_performersort(const QString &v);
|
||||||
void set_grouping(const QString &v);
|
void set_grouping(const QString &v);
|
||||||
void set_comment(const QString &v);
|
void set_comment(const QString &v);
|
||||||
void set_lyrics(const QString &v);
|
void set_lyrics(const QString &v);
|
||||||
@@ -317,6 +328,9 @@ class Song {
|
|||||||
void set_cue_path(const QString &v);
|
void set_cue_path(const QString &v);
|
||||||
|
|
||||||
void set_rating(const float 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_id(const QString &v);
|
||||||
void set_acoustid_fingerprint(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_stream_url(const QUrl &v);
|
||||||
|
|
||||||
void set_title(const TagLib::String &v);
|
void set_title(const TagLib::String &v);
|
||||||
|
void set_titlesort(const TagLib::String &v);
|
||||||
void set_album(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_artist(const TagLib::String &v);
|
||||||
|
void set_artistsort(const TagLib::String &v);
|
||||||
void set_albumartist(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_genre(const TagLib::String &v);
|
||||||
void set_composer(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_performer(const TagLib::String &v);
|
||||||
|
void set_performersort(const TagLib::String &v);
|
||||||
void set_grouping(const TagLib::String &v);
|
void set_grouping(const TagLib::String &v);
|
||||||
void set_comment(const TagLib::String &v);
|
void set_comment(const TagLib::String &v);
|
||||||
void set_lyrics(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_disc_id(const TagLib::String &v);
|
||||||
void set_musicbrainz_release_group_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_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() 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_album() const;
|
||||||
|
const QString &effective_albumsort() const;
|
||||||
|
const QString &effective_composersort() const;
|
||||||
|
const QString &effective_performersort() const;
|
||||||
int effective_originalyear() const;
|
int effective_originalyear() const;
|
||||||
const QString &playlist_albumartist() const;
|
const QString &playlist_effective_albumartist() const;
|
||||||
const QString &playlist_albumartist_sortable() const;
|
const QString &playlist_effective_albumartistsort() const;
|
||||||
|
|
||||||
bool is_metadata_good() const;
|
bool is_metadata_good() const;
|
||||||
bool is_local_collection_song() const;
|
bool is_local_collection_song() const;
|
||||||
@@ -402,6 +429,13 @@ class Song {
|
|||||||
bool comment_supported() const;
|
bool comment_supported() const;
|
||||||
bool lyrics_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);
|
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(); };
|
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;
|
bool IsEditable() const;
|
||||||
|
|
||||||
// Comparison functions
|
// Comparison functions
|
||||||
|
bool IsFileInfoEqual(const Song &other) const;
|
||||||
bool IsMetadataEqual(const Song &other) const;
|
bool IsMetadataEqual(const Song &other) const;
|
||||||
bool IsPlayStatisticsEqual(const Song &other) const;
|
bool IsPlayStatisticsEqual(const Song &other) const;
|
||||||
bool IsRatingEqual(const Song &other) const;
|
bool IsRatingEqual(const Song &other) const;
|
||||||
@@ -438,7 +473,10 @@ class Song {
|
|||||||
bool IsMusicBrainzEqual(const Song &other) const;
|
bool IsMusicBrainzEqual(const Song &other) const;
|
||||||
bool IsEBUR128Equal(const Song &other) const;
|
bool IsEBUR128Equal(const Song &other) const;
|
||||||
bool IsArtEqual(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 IsAllMetadataEqual(const Song &other) const;
|
||||||
|
bool IsEqual(const Song &other) const;
|
||||||
|
|
||||||
bool IsOnSameAlbum(const Song &other) const;
|
bool IsOnSameAlbum(const Song &other) const;
|
||||||
bool IsSimilar(const Song &other) const;
|
bool IsSimilar(const Song &other) const;
|
||||||
@@ -448,6 +486,7 @@ class Song {
|
|||||||
static QString DescriptionForSource(const Source source);
|
static QString DescriptionForSource(const Source source);
|
||||||
static Source SourceFromText(const QString &source);
|
static Source SourceFromText(const QString &source);
|
||||||
static QIcon IconForSource(const Source source);
|
static QIcon IconForSource(const Source source);
|
||||||
|
static QString DomainForSource(const Source source);
|
||||||
static QString TextForFiletype(const FileType filetype);
|
static QString TextForFiletype(const FileType filetype);
|
||||||
static QString ExtensionForFiletype(const FileType filetype);
|
static QString ExtensionForFiletype(const FileType filetype);
|
||||||
static QIcon IconForFiletype(const FileType filetype);
|
static QIcon IconForFiletype(const FileType filetype);
|
||||||
@@ -455,9 +494,12 @@ class Song {
|
|||||||
QString TextForSource() const { return TextForSource(source()); }
|
QString TextForSource() const { return TextForSource(source()); }
|
||||||
QString DescriptionForSource() const { return DescriptionForSource(source()); }
|
QString DescriptionForSource() const { return DescriptionForSource(source()); }
|
||||||
QIcon IconForSource() const { return IconForSource(source()); }
|
QIcon IconForSource() const { return IconForSource(source()); }
|
||||||
|
QString DomainForSource() const { return DomainForSource(source()); }
|
||||||
QString TextForFiletype() const { return TextForFiletype(filetype()); }
|
QString TextForFiletype() const { return TextForFiletype(filetype()); }
|
||||||
QIcon IconForFiletype() const { return IconForFiletype(filetype()); }
|
QIcon IconForFiletype() const { return IconForFiletype(filetype()); }
|
||||||
|
|
||||||
|
QString ShareURL() const;
|
||||||
|
|
||||||
bool IsFileLossless() const;
|
bool IsFileLossless() const;
|
||||||
static FileType FiletypeByMimetype(const QString &mimetype);
|
static FileType FiletypeByMimetype(const QString &mimetype);
|
||||||
static FileType FiletypeByDescription(const QString &text);
|
static FileType FiletypeByDescription(const QString &text);
|
||||||
@@ -521,9 +563,6 @@ class Song {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
struct Private;
|
struct Private;
|
||||||
|
|
||||||
static QString sortable(const QString &v);
|
|
||||||
|
|
||||||
QSharedDataPointer<Private> d;
|
QSharedDataPointer<Private> d;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -178,9 +178,11 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
|
|||||||
SongLoader::Result SongLoader::LoadAudioCD() {
|
SongLoader::Result SongLoader::LoadAudioCD() {
|
||||||
|
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
CddaSongLoader *cdda_song_loader = new CddaSongLoader(QUrl(), this);
|
CDDASongLoader *cdda_song_loader = new CDDASongLoader(QUrl(), this);
|
||||||
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsDurationLoaded, this, &SongLoader::AudioCDTracksLoadFinishedSlot);
|
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadError, this, &SongLoader::AudioCDTracksLoadErrorSlot);
|
||||||
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsMetadataLoaded, this, &SongLoader::AudioCDTracksTagsLoaded);
|
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();
|
cdda_song_loader->LoadSongs();
|
||||||
return Result::Success;
|
return Result::Success;
|
||||||
#else
|
#else
|
||||||
@@ -192,23 +194,38 @@ SongLoader::Result SongLoader::LoadAudioCD() {
|
|||||||
|
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
|
|
||||||
void SongLoader::AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error) {
|
void SongLoader::AudioCDTracksLoadErrorSlot(const QString &error) {
|
||||||
|
|
||||||
songs_ = songs;
|
|
||||||
errors_ << error;
|
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;
|
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) {
|
SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class ParserBase;
|
|||||||
class CueParser;
|
class CueParser;
|
||||||
|
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
class CddaSongLoader;
|
class CDDASongLoader;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
class SongLoader : public QObject {
|
class SongLoader : public QObject {
|
||||||
@@ -90,17 +90,21 @@ class SongLoader : public QObject {
|
|||||||
QStringList errors() { return errors_; }
|
QStringList errors() { return errors_; }
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void AudioCDTracksLoadFinished();
|
void AudioCDTracksLoaded();
|
||||||
void LoadAudioCDFinished(const bool success);
|
void AudioCDTracksUpdated();
|
||||||
|
void AudioCDLoadingFinished(const bool success);
|
||||||
void LoadRemoteFinished();
|
void LoadRemoteFinished();
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void ScheduleTimeout();
|
void ScheduleTimeout();
|
||||||
void Timeout();
|
void Timeout();
|
||||||
void StopTypefind();
|
void StopTypefind();
|
||||||
|
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
void AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error);
|
void AudioCDTracksLoadErrorSlot(const QString &error);
|
||||||
void AudioCDTracksTagsLoaded(const SongList &songs);
|
void AudioCDTracksLoadedSlot(const SongList &songs);
|
||||||
|
void AudioCDTracksUpdatedSlot(const SongList &songs);
|
||||||
|
void AudioCDLoadingFinishedSlot();
|
||||||
#endif // HAVE_AUDIOCD
|
#endif // HAVE_AUDIOCD
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -31,8 +31,8 @@
|
|||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QColor>
|
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
|
#include <QColor>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
|
|
||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
@@ -80,20 +80,13 @@ void StyleSheetLoader::UpdateStyleSheet(QWidget *widget, SharedPtr<StyleSheetDat
|
|||||||
// Replace %palette-role with actual colours
|
// Replace %palette-role with actual colours
|
||||||
QPalette p(widget->palette());
|
QPalette p(widget->palette());
|
||||||
|
|
||||||
{
|
QColor color_altbase = p.color(QPalette::AlternateBase);
|
||||||
QColor alt = p.color(QPalette::AlternateBase);
|
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
if (alt.lightness() > 180) {
|
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? (color_altbase.lightness() > 180 ? 130 : 16) : color_altbase.alpha());
|
||||||
alt.setAlpha(130);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
alt.setAlpha(16);
|
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
alt.setAlpha(130);
|
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? 116 : color_altbase.alpha());
|
||||||
#endif
|
#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"Window"_s, p, QPalette::Window);
|
||||||
ReplaceColor(&stylesheet, u"Background"_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)) {
|
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.
|
// Skip any provider that requires authentication but is not authenticated.
|
||||||
if (provider->authentication_required() && !provider->authenticated()) {
|
if (provider->authentication_required() && !provider->authenticated()) {
|
||||||
@@ -249,6 +249,8 @@ void AlbumCoverFetcherSearch::ProviderSearchFinished(const int id, const CoverPr
|
|||||||
|
|
||||||
void AlbumCoverFetcherSearch::AllProvidersFinished() {
|
void AlbumCoverFetcherSearch::AllProvidersFinished() {
|
||||||
|
|
||||||
|
qLog(Debug) << "Search finished, got" << results_.count() << "results";
|
||||||
|
|
||||||
if (cancel_requested_) {
|
if (cancel_requested_) {
|
||||||
return;
|
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);
|
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_; }
|
QString name() const { return name_; }
|
||||||
bool is_enabled() const { return enabled_; }
|
bool enabled() const { return enabled_; }
|
||||||
int order() const { return order_; }
|
int order() const { return order_; }
|
||||||
float quality() const { return quality_; }
|
float quality() const { return quality_; }
|
||||||
bool batch() const { return batch_; }
|
bool batch() const { return batch_; }
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ void CoverProviders::ReloadSettings() {
|
|||||||
QMap<int, QString> all_providers;
|
QMap<int, QString> all_providers;
|
||||||
QList<CoverProvider*> old_providers = cover_providers_.keys();
|
QList<CoverProvider*> old_providers = cover_providers_.keys();
|
||||||
for (CoverProvider *provider : std::as_const(old_providers)) {
|
for (CoverProvider *provider : std::as_const(old_providers)) {
|
||||||
if (!provider->is_enabled()) continue;
|
if (!provider->enabled()) continue;
|
||||||
all_providers.insert(provider->order(), provider->name());
|
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;
|
const QJsonObject &json_object = json_object_result.json_object;
|
||||||
if (!json_object.isEmpty()) {
|
if (json_object.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonArray array_data;
|
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();
|
array_data = json_object["data"_L1].toArray();
|
||||||
}
|
}
|
||||||
else if (json_object.contains("DATA"_L1) && json_object["DATA"_L1].isArray()) {
|
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 {
|
else {
|
||||||
Error(u"Json reply object is missing data."_s, json_object);
|
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;
|
QMap<QUrl, CoverProviderSearchResult> cover_results;
|
||||||
int i = 0;
|
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);
|
Error(u"Invalid Json reply, data array value is not a object."_s);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const QJsonObject value_object = json_value.toObject();
|
const QJsonObject object_entry = value_entry.toObject();
|
||||||
QJsonObject object_album;
|
QJsonObject object_album;
|
||||||
if (value_object.contains("album"_L1) && value_object["album"_L1].isObject()) { // Song search, so extract the album.
|
if (object_entry.contains("album"_L1) && object_entry["album"_L1].isObject()) { // Song search, so extract the album.
|
||||||
object_album = value_object["album"_L1].toObject();
|
object_album = object_entry["album"_L1].toObject();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
object_album = value_object;
|
object_album = object_entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value_object.contains("id"_L1) || !object_album.contains("id"_L1)) {
|
if (!object_entry.contains("id"_L1) || !object_album.contains("id"_L1)) {
|
||||||
Error(u"Invalid Json reply, data array value object is missing ID."_s, value_object);
|
Error(u"Invalid Json reply, data array value object is missing ID."_s, object_entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,11 +210,11 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!json_object.contains("artist"_L1)) {
|
if (!object_entry.contains("artist"_L1)) {
|
||||||
Error(u"Invalid Json reply, data array value object is missing artist."_s, json_object);
|
Error(u"Invalid Json reply, data array value object is missing artist."_s, object_entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const QJsonValue value_artist = json_object["artist"_L1];
|
const QJsonValue value_artist = object_entry["artist"_L1];
|
||||||
if (!value_artist.isObject()) {
|
if (!value_artist.isObject()) {
|
||||||
Error(u"Invalid Json reply, data array value artist is not a object."_s, value_artist);
|
Error(u"Invalid Json reply, data array value artist is not a object."_s, value_artist);
|
||||||
continue;
|
continue;
|
||||||
@@ -242,12 +242,12 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
|||||||
<< qMakePair(u"cover_big"_s, QSize(500, 500));
|
<< qMakePair(u"cover_big"_s, QSize(500, 500));
|
||||||
for (const QPair<QString, QSize> &cover_size : cover_sizes) {
|
for (const QPair<QString, QSize> &cover_size : cover_sizes) {
|
||||||
if (!object_album.contains(cover_size.first)) continue;
|
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) {
|
if (!have_cover) {
|
||||||
have_cover = true;
|
have_cover = true;
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
QUrl url(cover);
|
const QUrl url(cover);
|
||||||
if (!cover_results.contains(url)) {
|
if (!cover_results.contains(url)) {
|
||||||
cover_result.image_url = url;
|
cover_result.image_url = url;
|
||||||
cover_result.image_size = cover_size.second;
|
cover_result.image_size = cover_size.second;
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
#include "core/networkaccessmanager.h"
|
#include "core/networkaccessmanager.h"
|
||||||
#include "core/logging.h"
|
#include "core/logging.h"
|
||||||
#include "core/song.h"
|
|
||||||
#include "core/oauthenticator.h"
|
#include "core/oauthenticator.h"
|
||||||
#include "albumcoverfetcher.h"
|
#include "albumcoverfetcher.h"
|
||||||
#include "jsoncoverprovider.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 kApiUrl[] = "https://openapi.tidal.com/v2";
|
||||||
constexpr char kApiClientIdB64[] = "RHBwV3FpTEM4ZFJSV1RJaQ==";
|
constexpr char kApiClientIdB64[] = "RHBwV3FpTEM4ZFJSV1RJaQ==";
|
||||||
constexpr char kApiClientSecretB64[] = "cGk0QmxpclZXQWlteWpBc0RnWmZ5RmVlRzA2b3E1blVBVTljUW1IdFhDST0=";
|
constexpr char kApiClientSecretB64[] = "cGk0QmxpclZXQWlteWpBc0RnWmZ5RmVlRzA2b3E1blVBVTljUW1IdFhDST0=";
|
||||||
constexpr int kLimit = 10;
|
constexpr char kContentTypeHeader[] = "application/vnd.api+json";
|
||||||
constexpr const int kRequestsDelay = 1000;
|
constexpr int kSearchLimit = 6;
|
||||||
|
constexpr const int kRequestsDelay = 300;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
using std::make_shared;
|
using std::make_shared;
|
||||||
@@ -87,8 +87,7 @@ bool OpenTidalCoverProvider::StartSearch(const QString &artist, const QString &a
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchRequestPtr search_request = make_shared<SearchRequest>(id, artist, album, title);
|
search_requests_queue_.enqueue(make_shared<QueuedSearchRequest>(make_shared<SearchRequest>(id, artist, album, title)));
|
||||||
search_requests_queue_ << search_request;
|
|
||||||
|
|
||||||
if (!timer_flush_requests_->isActive()) {
|
if (!timer_flush_requests_->isActive()) {
|
||||||
timer_flush_requests_->start();
|
timer_flush_requests_->start();
|
||||||
@@ -109,8 +108,21 @@ void OpenTidalCoverProvider::FlushRequests() {
|
|||||||
return;
|
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()) {
|
if (!search_requests_queue_.isEmpty()) {
|
||||||
SendSearchRequest(search_requests_queue_.dequeue());
|
QueuedSearchRequestPtr queued_search_request = search_requests_queue_.dequeue();
|
||||||
|
SendSearchRequest(queued_search_request->search);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,14 +245,14 @@ void OpenTidalCoverProvider::SendSearchRequest(SearchRequestPtr search_request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QUrlQuery url_query;
|
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);
|
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);
|
url.setQuery(url_query);
|
||||||
QNetworkRequest network_request(url);
|
QNetworkRequest network_request(url);
|
||||||
network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
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()) {
|
if (oauth_->authenticated()) {
|
||||||
network_request.setRawHeader("Authorization", oauth_->authorization_header());
|
network_request.setRawHeader("Authorization", oauth_->authorization_header());
|
||||||
}
|
}
|
||||||
@@ -258,95 +270,247 @@ void OpenTidalCoverProvider::HandleSearchReply(QNetworkReply *reply, SearchReque
|
|||||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
|
|
||||||
CoverProviderSearchResults results;
|
const QScopeGuard search_finished = qScopeGuard([this, search_request]() {
|
||||||
const QScopeGuard search_finished = qScopeGuard([this, search_request, &results]() { Q_EMIT SearchFinished(search_request->id, results); });
|
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);
|
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||||
if (!json_object_result.success()) {
|
if (!json_object_result.success()) {
|
||||||
Error(json_object_result.error_message);
|
Error(json_object_result.error_message);
|
||||||
if (login_in_progress_) {
|
if (login_in_progress_) {
|
||||||
search_requests_queue_.prepend(search_request);
|
search_requests_queue_.prepend(make_shared<QueuedSearchRequest>(search_request));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QJsonObject &json_object = json_object_result.json_object;
|
const QJsonObject &json_object = json_object_result.json_object;
|
||||||
|
|
||||||
if (!json_object.contains("included"_L1) || !json_object["included"_L1].isArray()) {
|
if (!json_object.contains("included"_L1) || !json_object["included"_L1].isArray()) {
|
||||||
qLog(Error) << "OpenTidal: Json object is missing included.";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QJsonArray array_included = json_object["included"_L1].toArray();
|
const QJsonArray array_included = json_object["included"_L1].toArray();
|
||||||
if (array_included.isEmpty()) {
|
if (array_included.isEmpty()) {
|
||||||
return;
|
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;
|
int i = 0;
|
||||||
for (const auto &value_included : array_included) {
|
for (const auto &value_file : array_files) {
|
||||||
|
if (!value_file.isObject()) {
|
||||||
if (!value_included.isObject()) {
|
|
||||||
qLog(Error) << "OpenTidal: Invalid Json reply: Albums array value is not a object.";
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const QJsonObject object_included = value_included.toObject();
|
const QJsonObject object_file = value_file.toObject();
|
||||||
|
if (!object_file.contains("href"_L1) || !object_file["href"_L1].isString()) {
|
||||||
if (!object_included.contains("attributes"_L1) || !object_included["attributes"_L1].isObject()) {
|
|
||||||
qLog(Error) << "OpenTidal: Invalid Json reply: Included array item is missing attributes object." << object_included;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const QJsonObject object_attributes = object_included["attributes"_L1].toObject();
|
if (!object_file.contains("meta"_L1) || !object_file["meta"_L1].isObject()) {
|
||||||
|
|
||||||
if (!object_attributes.contains("title"_L1) || !object_attributes["title"_L1].isString()) {
|
|
||||||
qLog(Error) << "OpenTidal: Invalid Json reply: Attributes is missing title string." << object_attributes;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const QString href = object_file["href"_L1].toString();
|
||||||
if (!object_attributes.contains("imageLinks"_L1) || !object_attributes["imageLinks"_L1].isArray()) {
|
const QJsonObject object_meta = object_file["meta"_L1].toObject();
|
||||||
qLog(Error) << "OpenTidal: Invalid Json reply: Attributes is missing imageLinks object." << object_attributes;
|
if (!object_meta.contains("width"_L1) || !object_meta.contains("height"_L1)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const int width = object_meta["width"_L1].toInt();
|
||||||
const QString album = object_attributes["title"_L1].toString();
|
const int height = object_meta["height"_L1].toInt();
|
||||||
const QJsonArray array_imagelinks = object_attributes["imageLinks"_L1].toArray();
|
const QUrl url(href);
|
||||||
|
if (!url.isValid() || width < 640 || height < 640) continue;
|
||||||
for (const auto &value_imagelink : array_imagelinks) {
|
CoverProviderSearchResult cover_result;
|
||||||
if (!value_imagelink.isObject()) {
|
cover_result.artist = search_request->artist;
|
||||||
continue;
|
cover_result.album = albumcover_request->album_title;
|
||||||
}
|
cover_result.image_url = url;
|
||||||
const QJsonObject object_imagelink = value_imagelink.toObject();
|
cover_result.image_size = QSize(width, height);
|
||||||
if (!object_imagelink.contains("href"_L1) || !object_imagelink.contains("meta"_L1) || !object_imagelink["meta"_L1].isObject()) {
|
cover_result.number = ++i;
|
||||||
continue;
|
search_request->results << cover_result;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenTidalCoverProvider::FinishAllSearches() {
|
void OpenTidalCoverProvider::FinishAllSearches() {
|
||||||
|
|
||||||
timer_flush_requests_->stop();
|
|
||||||
|
|
||||||
while (!search_requests_queue_.isEmpty()) {
|
while (!search_requests_queue_.isEmpty()) {
|
||||||
SearchRequestPtr search_request = search_requests_queue_.dequeue();
|
QueuedSearchRequestPtr queued_search_request = search_requests_queue_.dequeue();
|
||||||
Q_EMIT SearchFinished(search_request->id, CoverProviderSearchResults());
|
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) {
|
void OpenTidalCoverProvider::Error(const QString &error, const QVariant &debug) {
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
#include "jsoncoverprovider.h"
|
#include "jsoncoverprovider.h"
|
||||||
|
|
||||||
|
class QTimer;
|
||||||
class QNetworkReply;
|
class QNetworkReply;
|
||||||
class NetworkAccessManager;
|
class NetworkAccessManager;
|
||||||
class QTimer;
|
|
||||||
class OAuthenticator;
|
class OAuthenticator;
|
||||||
|
|
||||||
class OpenTidalCoverProvider : public JsonCoverProvider {
|
class OpenTidalCoverProvider : public JsonCoverProvider {
|
||||||
@@ -45,20 +45,68 @@ class OpenTidalCoverProvider : public JsonCoverProvider {
|
|||||||
void CancelSearch(const int id) override;
|
void CancelSearch(const int id) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct SearchRequest {
|
class ArtworkRequest {
|
||||||
explicit SearchRequest(const int _id, const QString &_artist, const QString &_album, const QString &_title) : id(_id), artist(_artist), album(_album), title(_title) {}
|
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;
|
int id;
|
||||||
QString artist;
|
QString artist;
|
||||||
QString album;
|
QString album;
|
||||||
QString title;
|
QString title;
|
||||||
|
QList<AlbumCoverRequestPtr> albumcover_requests;
|
||||||
|
CoverProviderSearchResults results;
|
||||||
|
bool finished;
|
||||||
};
|
};
|
||||||
using SearchRequestPtr = SharedPtr<SearchRequest>;
|
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:
|
private:
|
||||||
void LoginCheck();
|
void LoginCheck();
|
||||||
void Login();
|
void Login();
|
||||||
void SendSearchRequest(SearchRequestPtr request);
|
|
||||||
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
|
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 FinishAllSearches();
|
||||||
void Error(const QString &error, const QVariant &debug = QVariant()) override;
|
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 OAuthFinished(const bool success, const QString &error = QString());
|
||||||
void FlushRequests();
|
void FlushRequests();
|
||||||
void HandleSearchReply(QNetworkReply *reply, OpenTidalCoverProvider::SearchRequestPtr search_request);
|
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:
|
private:
|
||||||
OAuthenticator *oauth_;
|
OAuthenticator *oauth_;
|
||||||
QTimer *timer_flush_requests_;
|
QTimer *timer_flush_requests_;
|
||||||
bool login_in_progress_;
|
bool login_in_progress_;
|
||||||
QDateTime last_login_attempt_;
|
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
|
#endif // OPENTIDALCOVERPROVIDER_H
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -21,10 +21,19 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include <cdio/types.h>
|
||||||
|
#include <cdio/cdio.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "core/logging.h"
|
||||||
#include "collection/collectionmodel.h"
|
#include "collection/collectionmodel.h"
|
||||||
#include "cddasongloader.h"
|
#include "cddasongloader.h"
|
||||||
#include "connecteddevice.h"
|
#include "connecteddevice.h"
|
||||||
@@ -33,7 +42,9 @@
|
|||||||
class DeviceLister;
|
class DeviceLister;
|
||||||
class DeviceManager;
|
class DeviceManager;
|
||||||
|
|
||||||
CddaDevice::CddaDevice(const QUrl &url,
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
CDDADevice::CDDADevice(const QUrl &url,
|
||||||
DeviceLister *lister,
|
DeviceLister *lister,
|
||||||
const QString &unique_id,
|
const QString &unique_id,
|
||||||
DeviceManager *device_manager,
|
DeviceManager *device_manager,
|
||||||
@@ -45,36 +56,86 @@ CddaDevice::CddaDevice(const QUrl &url,
|
|||||||
const bool first_time,
|
const bool first_time,
|
||||||
QObject *parent)
|
QObject *parent)
|
||||||
: ConnectedDevice(url, lister, unique_id, device_manager, task_manager, database, tagreader_client, albumcover_loader, database_id, first_time, 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);
|
timer_disc_changed_->setInterval(1s);
|
||||||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsDurationLoaded, this, &CddaDevice::SongsLoaded);
|
|
||||||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsMetadataLoaded, this, &CddaDevice::SongsLoaded);
|
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsLoaded, this, &CDDADevice::SongsLoaded);
|
||||||
QObject::connect(this, &CddaDevice::SongsDiscovered, collection_model_, &CollectionModel::AddReAddOrUpdate);
|
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;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CddaDevice::Refresh() {
|
void CDDADevice::WatchForDiscChanges(const bool watch) {
|
||||||
|
|
||||||
if (!cdda_song_loader_.HasChanged()) {
|
if (watch && !timer_disc_changed_->isActive()) {
|
||||||
return;
|
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();
|
collection_model_->Reset();
|
||||||
Q_EMIT SongsDiscovered(songs);
|
Q_EMIT SongsDiscovered(songs);
|
||||||
song_count_ = songs.size();
|
song_count_ = songs.size();
|
||||||
|
(void)cdio_get_media_changed(cdio_);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CDDADevice::SongLoadingFinished() {
|
||||||
|
|
||||||
|
WatchForDiscChanges(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -24,6 +24,11 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include <cdio/types.h>
|
||||||
|
#include <cdio/cdio.h>
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
@@ -35,6 +40,8 @@
|
|||||||
#include "cddasongloader.h"
|
#include "cddasongloader.h"
|
||||||
#include "connecteddevice.h"
|
#include "connecteddevice.h"
|
||||||
|
|
||||||
|
class QTimer;
|
||||||
|
|
||||||
class DeviceLister;
|
class DeviceLister;
|
||||||
class DeviceManager;
|
class DeviceManager;
|
||||||
class TaskManager;
|
class TaskManager;
|
||||||
@@ -42,11 +49,11 @@ class Database;
|
|||||||
class TagReaderClient;
|
class TagReaderClient;
|
||||||
class AlbumCoverLoader;
|
class AlbumCoverLoader;
|
||||||
|
|
||||||
class CddaDevice : public ConnectedDevice {
|
class CDDADevice : public ConnectedDevice {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Q_INVOKABLE explicit CddaDevice(const QUrl &url,
|
Q_INVOKABLE explicit CDDADevice(const QUrl &url,
|
||||||
DeviceLister *lister,
|
DeviceLister *lister,
|
||||||
const QString &unique_id,
|
const QString &unique_id,
|
||||||
DeviceManager *device_manager,
|
DeviceManager *device_manager,
|
||||||
@@ -58,21 +65,29 @@ class CddaDevice : public ConnectedDevice {
|
|||||||
const bool first_time,
|
const bool first_time,
|
||||||
QObject *parent = nullptr);
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
|
~CDDADevice();
|
||||||
|
|
||||||
bool Init() override;
|
bool Init() override;
|
||||||
void Refresh() override;
|
|
||||||
bool CopyToStorage(const CopyJob&, QString&) override { return false; }
|
bool CopyToStorage(const CopyJob&, QString&) override { return false; }
|
||||||
bool DeleteFromStorage(const MusicStorage::DeleteJob&) override { return false; }
|
bool DeleteFromStorage(const MusicStorage::DeleteJob&) override { return false; }
|
||||||
|
|
||||||
static QStringList url_schemes() { return QStringList() << QStringLiteral("cdda"); }
|
static QStringList url_schemes() { return QStringList() << QStringLiteral("cdda"); }
|
||||||
|
|
||||||
|
void LoadSongs();
|
||||||
|
void WatchForDiscChanges(const bool watch);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void SongsDiscovered(const SongList &songs);
|
void SongsDiscovered(const SongList &songs);
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void SongsLoaded(const SongList &songs);
|
void CheckDiscChanged();
|
||||||
|
void SongsLoaded(const SongList &songs = SongList());
|
||||||
|
void SongLoadingFinished();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CddaSongLoader cdda_song_loader_;
|
CDDASongLoader cdda_song_loader_;
|
||||||
|
CdIo_t *cdio_;
|
||||||
|
QTimer *timer_disc_changed_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // CDDADEVICE_H
|
#endif // CDDADEVICE_H
|
||||||
|
|||||||
@@ -40,9 +40,9 @@
|
|||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
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)
|
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_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||||
cdio_hwinfo_t cd_info;
|
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_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||||
cdio_hwinfo_t cd_info;
|
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)
|
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)
|
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)
|
Q_UNUSED(id)
|
||||||
return QVariantMap();
|
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_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||||
cdio_hwinfo_t cd_info;
|
cdio_hwinfo_t cd_info;
|
||||||
if (cdio_get_hwinfo(cdio, &cd_info)) {
|
if (cdio_get_hwinfo(cdio, &cd_info)) {
|
||||||
|
const QString friendly_name = QString::fromUtf8(cd_info.psz_model).trimmed();
|
||||||
cdio_destroy(cdio);
|
cdio_destroy(cdio);
|
||||||
return QString::fromUtf8(cd_info.psz_model);
|
return friendly_name;
|
||||||
}
|
}
|
||||||
cdio_destroy(cdio);
|
cdio_destroy(cdio);
|
||||||
return u"CD ("_s + id + QLatin1Char(')');
|
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);
|
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());
|
cdio_eject_media_drive(id.toLocal8Bit().constData());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CddaLister::UpdateDeviceFreeSpace(const QString &id) {
|
void CDDALister::UpdateDeviceFreeSpace(const QString &id) {
|
||||||
Q_UNUSED(id)
|
Q_UNUSED(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CddaLister::Init() {
|
bool CDDALister::Init() {
|
||||||
|
|
||||||
cdio_init();
|
cdio_init();
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
|
|||||||
@@ -34,11 +34,11 @@
|
|||||||
|
|
||||||
#include "devicelister.h"
|
#include "devicelister.h"
|
||||||
|
|
||||||
class CddaLister : public DeviceLister {
|
class CDDALister : public DeviceLister {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit CddaLister(QObject *parent = nullptr) : DeviceLister(parent) {}
|
explicit CDDALister(QObject *parent = nullptr) : DeviceLister(parent) {}
|
||||||
|
|
||||||
QStringList DeviceUniqueIDs() override;
|
QStringList DeviceUniqueIDs() override;
|
||||||
QVariantList DeviceIcons(const QString &id) override;
|
QVariantList DeviceIcons(const QString &id) override;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -21,25 +21,24 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
#include <memory>
|
#include <QtGlobal>
|
||||||
|
|
||||||
#include <cstddef>
|
#include <memory>
|
||||||
|
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
#include <glib/gtypes.h>
|
#include <glib/gtypes.h>
|
||||||
#include <glib-object.h>
|
#include <glib-object.h>
|
||||||
|
|
||||||
#include <cdio/cdio.h>
|
|
||||||
|
|
||||||
#include <gst/gst.h>
|
#include <gst/gst.h>
|
||||||
#include <gst/tag/tag.h>
|
#include <gst/tag/tag.h>
|
||||||
|
|
||||||
#include <QtGlobal>
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
#include <QtConcurrentRun>
|
||||||
|
#include <QScopeGuard>
|
||||||
|
|
||||||
#include "cddasongloader.h"
|
#include "cddasongloader.h"
|
||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
@@ -51,18 +50,27 @@ using std::make_shared;
|
|||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
CddaSongLoader::CddaSongLoader(const QUrl &url, QObject *parent)
|
CDDASongLoader::CDDASongLoader(const QUrl &url, QObject *parent)
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
url_(url),
|
url_(url),
|
||||||
network_(make_shared<NetworkAccessManager>()),
|
network_(make_shared<NetworkAccessManager>()),
|
||||||
cdda_(nullptr),
|
#ifdef HAVE_MUSICBRAINZ
|
||||||
cdio_(nullptr) {}
|
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()) {
|
if (url_.isEmpty()) {
|
||||||
return QUrl(QStringLiteral("cdda://%1a").arg(track_number));
|
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_);
|
if (IsActive()) {
|
||||||
cdio_ = cdio_open(url_.path().toLocal8Bit().constData(), DRIVER_DEVICE);
|
|
||||||
if (cdio_ == nullptr) {
|
|
||||||
Error(u"Unable to open CDIO device."_s);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create gstreamer cdda element
|
loading_future_ = QtConcurrent::run(&CDDASongLoader::LoadSongsFromCDDA, this);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void CDDASongLoader::LoadSongsFromCDDA() {
|
||||||
|
|
||||||
|
QMutexLocker l(&mutex_load_);
|
||||||
|
|
||||||
GError *error = nullptr;
|
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) {
|
if (error) {
|
||||||
Error(QStringLiteral("%1: %2").arg(error->code).arg(QString::fromUtf8(error->message)));
|
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()) {
|
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")) {
|
if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda), "paranoia-mode")) {
|
||||||
g_object_set(cdda_, "paranoia-mode", 0, nullptr);
|
g_object_set(cdda, "paranoia-mode", 0, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the element's state to ready and paused, to be able to query it
|
// 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) {
|
if (gst_element_set_state(cdda, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) {
|
||||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||||
gst_object_unref(GST_OBJECT(cdda_));
|
gst_object_unref(GST_OBJECT(cdda));
|
||||||
cdda_ = nullptr;
|
cdda = nullptr;
|
||||||
Error(tr("Error while setting CDDA device to ready state."));
|
Error(tr("Error while setting CDDA device to ready state."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gst_element_set_state(cdda_, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
|
if (gst_element_set_state(cdda, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
|
||||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||||
gst_object_unref(GST_OBJECT(cdda_));
|
gst_object_unref(GST_OBJECT(cdda));
|
||||||
cdda_ = nullptr;
|
cdda = nullptr;
|
||||||
Error(tr("Error while setting CDDA device to pause state."));
|
Error(tr("Error while setting CDDA device to pause state."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get number of tracks
|
// Get number of tracks
|
||||||
GstFormat fmt = gst_format_get_by_nick("track");
|
GstFormat format_track = gst_format_get_by_nick("track");
|
||||||
GstFormat out_fmt = fmt;
|
GstFormat format_duration = format_track;
|
||||||
gint64 num_tracks = 0;
|
gint64 total_tracks = 0;
|
||||||
if (!gst_element_query_duration(cdda_, out_fmt, &num_tracks)) {
|
if (!gst_element_query_duration(cdda, format_duration, &total_tracks)) {
|
||||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||||
gst_object_unref(GST_OBJECT(cdda_));
|
gst_object_unref(GST_OBJECT(cdda));
|
||||||
cdda_ = nullptr;
|
cdda = nullptr;
|
||||||
Error(tr("Error while querying CDDA tracks."));
|
Error(tr("Error while querying CDDA tracks."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (out_fmt != fmt) {
|
if (format_duration != format_track) {
|
||||||
qLog(Error) << "Error while querying cdda GstElement (2).";
|
qLog(Error) << "Error while querying CDDA GstElement (2).";
|
||||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||||
gst_object_unref(GST_OBJECT(cdda_));
|
gst_object_unref(GST_OBJECT(cdda));
|
||||||
cdda_ = nullptr;
|
cdda = nullptr;
|
||||||
Error(tr("Error while querying CDDA tracks."));
|
Error(tr("Error while querying CDDA tracks."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SongList songs;
|
QMap<int, Song> songs;
|
||||||
songs.reserve(num_tracks);
|
for (int track_number = 1; track_number <= total_tracks; ++track_number) {
|
||||||
for (int track_number = 1; track_number <= num_tracks; ++track_number) {
|
|
||||||
// Init song
|
|
||||||
Song song(Song::Source::CDDA);
|
Song song(Song::Source::CDDA);
|
||||||
song.set_id(track_number);
|
song.set_id(track_number);
|
||||||
song.set_valid(true);
|
song.set_valid(true);
|
||||||
@@ -145,129 +158,279 @@ void CddaSongLoader::LoadSongs() {
|
|||||||
song.set_url(GetUrlFromTrack(track_number));
|
song.set_url(GetUrlFromTrack(track_number));
|
||||||
song.set_title(QStringLiteral("Track %1").arg(track_number));
|
song.set_title(QStringLiteral("Track %1").arg(track_number));
|
||||||
song.set_track(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();
|
gst_tag_register_musicbrainz_tags();
|
||||||
|
#endif // HAVE_MUSICBRAINZ
|
||||||
|
|
||||||
GstElement *pipeline = gst_pipeline_new("pipeline");
|
GstElement *pipeline = gst_pipeline_new("pipeline");
|
||||||
GstElement *sink = gst_element_factory_make("fakesink", nullptr);
|
GstElement *sink = gst_element_factory_make("fakesink", nullptr);
|
||||||
gst_bin_add_many(GST_BIN(pipeline), cdda_, sink, nullptr);
|
gst_bin_add_many(GST_BIN(pipeline), cdda, sink, nullptr);
|
||||||
gst_element_link(cdda_, sink);
|
gst_element_link(cdda, sink);
|
||||||
gst_element_set_state(pipeline, GST_STATE_READY);
|
gst_element_set_state(pipeline, GST_STATE_READY);
|
||||||
gst_element_set_state(pipeline, GST_STATE_PAUSED);
|
gst_element_set_state(pipeline, GST_STATE_PAUSED);
|
||||||
|
|
||||||
// Get TOC and TAG messages
|
|
||||||
GstMessage *msg = nullptr;
|
GstMessage *msg = nullptr;
|
||||||
GstMessage *msg_toc = nullptr;
|
int track_artist_tags = 0;
|
||||||
GstMessage *msg_tag = nullptr;
|
int track_album_tags = 0;
|
||||||
while ((msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND, static_cast<GstMessageType>(GST_MESSAGE_TOC | GST_MESSAGE_TAG)))) {
|
int track_title_tags = 0;
|
||||||
if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TOC) {
|
#ifdef HAVE_MUSICBRAINZ
|
||||||
if (msg_toc) gst_message_unref(msg_toc); // Shouldn't happen, but just in case
|
QString musicbrainz_discid;
|
||||||
msg_toc = msg;
|
#endif // HAVE_MUSICBRAINZ
|
||||||
}
|
GstMessageType msg_filter = static_cast<GstMessageType>(GST_MESSAGE_TOC|GST_MESSAGE_TAG);
|
||||||
else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
|
while (msg_filter != 0 && (msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND * 5, msg_filter))) {
|
||||||
if (msg_tag) gst_message_unref(msg_tag);
|
|
||||||
msg_tag = msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle TOC message: get tracks duration
|
const QScopeGuard scopeguard_msg = qScopeGuard([msg]() {
|
||||||
if (msg_toc) {
|
gst_message_unref(msg);
|
||||||
GstToc *toc = nullptr;
|
});
|
||||||
gst_message_parse_toc(msg_toc, &toc, nullptr);
|
|
||||||
if (toc) {
|
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);
|
GList *entries = gst_toc_get_entries(toc);
|
||||||
if (entries && static_cast<guint>(songs.size()) <= g_list_length(entries)) {
|
int track_number = 0;
|
||||||
int i = 0;
|
for (GList *entry_node = entries; entry_node != nullptr; entry_node = entry_node->next) {
|
||||||
for (GList *node = entries; node != nullptr; node = node->next) {
|
++track_number;
|
||||||
GstTocEntry *entry = static_cast<GstTocEntry*>(node->data);
|
if (songs.contains(track_number)) {
|
||||||
qint64 duration = 0;
|
Song &song = songs[track_number];
|
||||||
|
GstTocEntry *entry = static_cast<GstTocEntry*>(entry_node->data);
|
||||||
gint64 start = 0, stop = 0;
|
gint64 start = 0, stop = 0;
|
||||||
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) duration = stop - start;
|
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) {
|
||||||
songs[i++].set_length_nanosec(duration);
|
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);
|
|
||||||
}
|
else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
|
||||||
Q_EMIT SongsDurationLoaded(songs);
|
|
||||||
|
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
|
#ifdef HAVE_MUSICBRAINZ
|
||||||
// Handle TAG message: generate MusicBrainz DiscId
|
if (musicbrainz_discid.isEmpty()) {
|
||||||
if (msg_tag) {
|
if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID, &tag)) {
|
||||||
GstTagList *tags = nullptr;
|
musicbrainz_discid = QString::fromUtf8(tag);
|
||||||
gst_message_parse_tag(msg_tag, &tags);
|
g_free(tag);
|
||||||
char *string_mb = nullptr;
|
tag = 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;
|
#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);
|
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);
|
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
|
#ifdef HAVE_MUSICBRAINZ
|
||||||
void CddaSongLoader::AudioCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results) {
|
|
||||||
|
|
||||||
MusicBrainzClient *musicbrainz_client = qobject_cast<MusicBrainzClient*>(sender());
|
void CDDASongLoader::LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid, const QMap<int, Song> &songs) {
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
musicbrainz_discid_ = musicbrainz_discid;
|
||||||
#endif
|
musicbrainz_songs_ = songs;
|
||||||
|
musicbrainz_client_->StartDiscIdRequest(musicbrainz_discid);
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
qLog(Error) << error;
|
||||||
Q_EMIT SongsDurationLoaded(SongList(), error);
|
|
||||||
|
Q_EMIT LoadError(error);
|
||||||
|
Q_EMIT LoadingFinished();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -24,16 +24,13 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
|
|
||||||
#include <cdio/types.h>
|
|
||||||
#include <cdio/cdio.h>
|
|
||||||
|
|
||||||
#include <gst/gstelement.h>
|
#include <gst/gstelement.h>
|
||||||
#include <gst/audio/gstaudiocdsrc.h>
|
#include <gst/audio/gstaudiocdsrc.h>
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
|
#include <QFuture>
|
||||||
|
#include <QMap>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
@@ -45,39 +42,48 @@
|
|||||||
|
|
||||||
class NetworkAccessManager;
|
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
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit CddaSongLoader(const QUrl &url, QObject *parent = nullptr);
|
explicit CDDASongLoader(const QUrl &url, QObject *parent = nullptr);
|
||||||
~CddaSongLoader() override;
|
~CDDASongLoader() override;
|
||||||
|
|
||||||
// Load songs. Signals declared below will be emitted anytime new information will be available.
|
|
||||||
void LoadSongs();
|
void LoadSongs();
|
||||||
bool HasChanged();
|
|
||||||
|
bool IsActive() const { return loading_future_.isRunning(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void LoadSongsFromCDDA();
|
||||||
void Error(const QString &error);
|
void Error(const QString &error);
|
||||||
QUrl GetUrlFromTrack(const int track_number) const;
|
QUrl GetUrlFromTrack(const int track_number) const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void SongsLoadError(const QString &error);
|
|
||||||
void SongsLoaded(const SongList &songs);
|
void SongsLoaded(const SongList &songs);
|
||||||
void SongsDurationLoaded(const SongList &songs, const QString &error = QString());
|
void SongsUpdated(const SongList &songs);
|
||||||
void SongsMetadataLoaded(const SongList &songs);
|
void LoadError(const QString &error);
|
||||||
|
void LoadingFinished();
|
||||||
|
void LoadTagsFromMusicBrainz(const QString &musicbrainz_discid, const QMap<int, Song> &songs);
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
#ifdef HAVE_MUSICBRAINZ
|
#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
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const QUrl url_;
|
const QUrl url_;
|
||||||
SharedPtr<NetworkAccessManager> network_;
|
SharedPtr<NetworkAccessManager> network_;
|
||||||
GstElement *cdda_;
|
#ifdef HAVE_MUSICBRAINZ
|
||||||
CdIo_t *cdio_;
|
MusicBrainzClient *musicbrainz_client_;
|
||||||
|
#endif
|
||||||
QMutex mutex_load_;
|
QMutex mutex_load_;
|
||||||
|
QFuture<void> loading_future_;
|
||||||
|
#ifdef HAVE_MUSICBRAINZ
|
||||||
|
QString musicbrainz_discid_;
|
||||||
|
QMap<int, Song> musicbrainz_songs_;
|
||||||
|
#endif
|
||||||
|
bool whatever_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // CDDASONGLOADER_H
|
#endif // CDDASONGLOADER_H
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ class ConnectedDevice : public QObject, public virtual MusicStorage, public enab
|
|||||||
virtual bool IsLoading() { return false; }
|
virtual bool IsLoading() { return false; }
|
||||||
virtual void NewConnection() {}
|
virtual void NewConnection() {}
|
||||||
virtual void ConnectAsync();
|
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;
|
TranscodeMode GetTranscodeMode() const override;
|
||||||
Song::FileType GetTranscodeFormat() const override;
|
Song::FileType GetTranscodeFormat() const override;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int kDeviceSchemaVersion = 5;
|
constexpr int kDeviceSchemaVersion = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceDatabaseBackend::DeviceDatabaseBackend(QObject *parent)
|
DeviceDatabaseBackend::DeviceDatabaseBackend(QObject *parent)
|
||||||
|
|||||||
@@ -36,61 +36,75 @@
|
|||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
DeviceDatabaseBackend::Device DeviceInfo::SaveToDb() const {
|
void DeviceInfo::InitFromDb(const DeviceDatabaseBackend::Device &device) {
|
||||||
|
|
||||||
DeviceDatabaseBackend::Device ret;
|
database_id_ = device.id_;
|
||||||
ret.friendly_name_ = friendly_name_;
|
friendly_name_ = device.friendly_name_;
|
||||||
ret.size_ = size_;
|
size_ = device.size_;
|
||||||
ret.id_ = database_id_;
|
transcode_mode_ = device.transcode_mode_;
|
||||||
ret.icon_name_ = icon_name_;
|
transcode_format_ = device.transcode_format_;
|
||||||
ret.transcode_mode_ = transcode_mode_;
|
icon_name_ = device.icon_name_;
|
||||||
ret.transcode_format_ = transcode_format_;
|
|
||||||
|
|
||||||
QStringList unique_ids;
|
InitIcon();
|
||||||
unique_ids.reserve(backends_.count());
|
|
||||||
for (const Backend &backend : backends_) {
|
|
||||||
unique_ids << backend.unique_id_;
|
|
||||||
}
|
|
||||||
ret.unique_id_ = unique_ids.join(u',');
|
|
||||||
|
|
||||||
return ret;
|
const QStringList unique_ids = device.unique_id_.split(u',');
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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',');
|
|
||||||
for (const QString &id : unique_ids) {
|
for (const QString &id : unique_ids) {
|
||||||
backends_ << Backend(nullptr, id);
|
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 {
|
const DeviceInfo::Backend *DeviceInfo::BestBackend() const {
|
||||||
|
|
||||||
int best_priority = -1;
|
int best_priority = -1;
|
||||||
const Backend *ret = nullptr;
|
const Backend *backend = nullptr;
|
||||||
|
|
||||||
for (int i = 0; i < backends_.count(); ++i) {
|
for (int i = 0; i < backends_.count(); ++i) {
|
||||||
if (backends_[i].lister_ && backends_[i].lister_->priority() > best_priority) {
|
if (backends_[i].lister_ && backends_[i].lister_->priority() > best_priority) {
|
||||||
best_priority = backends_[i].lister_->priority();
|
best_priority = backends_[i].lister_->priority();
|
||||||
ret = &(backends_[i]);
|
backend = &(backends_[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ret && !backends_.isEmpty()) return &(backends_[0]);
|
if (!backend && !backends_.isEmpty()) return &(backends_[0]);
|
||||||
return ret;
|
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;
|
icon_name_ = "device"_L1;
|
||||||
|
|
||||||
|
|||||||
@@ -97,8 +97,9 @@ class DeviceInfo : public SimpleTreeItem<DeviceInfo> {
|
|||||||
void InitFromDb(const DeviceDatabaseBackend::Device &dev);
|
void InitFromDb(const DeviceDatabaseBackend::Device &dev);
|
||||||
DeviceDatabaseBackend::Device SaveToDb() const;
|
DeviceDatabaseBackend::Device SaveToDb() const;
|
||||||
|
|
||||||
|
void InitIcon();
|
||||||
// Tries to load a good icon for the device. Sets icon_name_ and icon_.
|
// 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)
|
// Gets the best backend available (the one with the highest priority)
|
||||||
const Backend *BestBackend() const;
|
const Backend *BestBackend() const;
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
|
|||||||
backend_->moveToThread(database->thread());
|
backend_->moveToThread(database->thread());
|
||||||
backend_->Init(database);
|
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.
|
// 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);
|
(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.
|
// CD devices are detected via the DiskArbitration framework instead on MacOs.
|
||||||
#if defined(HAVE_AUDIOCD) && !defined(Q_OS_MACOS)
|
#if defined(HAVE_AUDIOCD) && !defined(Q_OS_MACOS)
|
||||||
AddLister(new CddaLister);
|
AddLister(new CDDALister);
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_UDISKS2
|
#ifdef HAVE_UDISKS2
|
||||||
AddLister(new Udisks2Lister);
|
AddLister(new Udisks2Lister);
|
||||||
@@ -133,7 +133,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
AddDeviceClass<CddaDevice>();
|
AddDeviceClass<CDDADevice>();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
AddDeviceClass<FilesystemDevice>();
|
AddDeviceClass<FilesystemDevice>();
|
||||||
@@ -167,12 +167,12 @@ void DeviceManager::Exit() {
|
|||||||
|
|
||||||
void DeviceManager::CloseDevices() {
|
void DeviceManager::CloseDevices() {
|
||||||
|
|
||||||
for (DeviceInfo *info : std::as_const(devices_)) {
|
for (DeviceInfo *device_info : std::as_const(devices_)) {
|
||||||
if (!info->device_) continue;
|
if (!device_info->device_) continue;
|
||||||
if (wait_for_exit_.contains(&*info->device_)) continue;
|
if (wait_for_exit_.contains(&*device_info->device_)) continue;
|
||||||
wait_for_exit_ << &*info->device_;
|
wait_for_exit_ << &*device_info->device_;
|
||||||
QObject::connect(&*info->device_, &ConnectedDevice::destroyed, this, &DeviceManager::DeviceDestroyed);
|
QObject::connect(&*device_info->device_, &ConnectedDevice::destroyed, this, &DeviceManager::DeviceDestroyed);
|
||||||
info->device_->Close();
|
device_info->device_->Close();
|
||||||
}
|
}
|
||||||
if (wait_for_exit_.isEmpty()) CloseListers();
|
if (wait_for_exit_.isEmpty()) CloseListers();
|
||||||
|
|
||||||
@@ -224,10 +224,10 @@ void DeviceManager::ListerClosed() {
|
|||||||
|
|
||||||
void DeviceManager::DeviceDestroyed() {
|
void DeviceManager::DeviceDestroyed() {
|
||||||
|
|
||||||
ConnectedDevice *device = static_cast<ConnectedDevice*>(sender());
|
ConnectedDevice *connected_device = static_cast<ConnectedDevice*>(sender());
|
||||||
if (!wait_for_exit_.contains(device) || !backend_) return;
|
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();
|
if (wait_for_exit_.isEmpty()) CloseListers();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -237,41 +237,37 @@ void DeviceManager::LoadAllDevices() {
|
|||||||
Q_ASSERT(QThread::currentThread() != qApp->thread());
|
Q_ASSERT(QThread::currentThread() != qApp->thread());
|
||||||
|
|
||||||
const DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices();
|
const DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices();
|
||||||
for (const DeviceDatabaseBackend::Device &device : devices) {
|
|
||||||
DeviceInfo *info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
Q_EMIT DevicesLoaded(devices);
|
||||||
info->InitFromDb(device);
|
|
||||||
Q_EMIT DeviceCreatedFromDB(info);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is done in a concurrent thread so close the unique DB connection.
|
// This is done in a concurrent thread so close the unique DB connection.
|
||||||
backend_->Close();
|
backend_->Close();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeviceManager::AddDeviceFromDB(DeviceInfo *info) {
|
void DeviceManager::AddDevicesFromDB(const DeviceDatabaseBackend::DeviceList &devices) {
|
||||||
|
|
||||||
const QStringList icon_names = info->icon_name_.split(u',');
|
for (const DeviceDatabaseBackend::Device &device : devices) {
|
||||||
QVariantList icons;
|
const QStringList unique_ids = device.unique_id_.split(u',');
|
||||||
icons.reserve(icon_names.count());
|
DeviceInfo *device_info = FindEquivalentDevice(unique_ids);
|
||||||
for (const QString &icon_name : icon_names) {
|
if (device_info && device_info->database_id_ == -1) {
|
||||||
icons << icon_name;
|
qLog(Info) << "Database device linked to physical device:" << device.friendly_name_;
|
||||||
}
|
device_info->database_id_ = device.id_;
|
||||||
info->SetIcon(icons, info->friendly_name_);
|
device_info->icon_name_ = device.icon_name_;
|
||||||
|
device_info->InitIcon();
|
||||||
DeviceInfo *existing = FindEquivalentDevice(info);
|
const QModelIndex idx = ItemToIndex(device_info);
|
||||||
if (existing) {
|
if (idx.isValid()) {
|
||||||
qLog(Info) << "Found existing device: " << info->friendly_name_;
|
Q_EMIT dataChanged(idx, idx);
|
||||||
existing->icon_name_ = info->icon_name_;
|
}
|
||||||
existing->icon_ = info->icon_;
|
}
|
||||||
QModelIndex idx = ItemToIndex(existing);
|
else {
|
||||||
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
|
qLog(Info) << "Database device:" << device.friendly_name_;
|
||||||
root_->Delete(info->row);
|
device_info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||||
}
|
device_info->InitFromDb(device);
|
||||||
else {
|
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
|
||||||
qLog(Info) << "Device added from database: " << info->friendly_name_;
|
devices_ << device_info;
|
||||||
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
|
endInsertRows();
|
||||||
devices_ << info;
|
}
|
||||||
endInsertRows();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -280,30 +276,29 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
|
|||||||
|
|
||||||
if (!idx.isValid() || idx.column() != 0) return QVariant();
|
if (!idx.isValid() || idx.column() != 0) return QVariant();
|
||||||
|
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
if (!info) return QVariant();
|
if (!device_info) return QVariant();
|
||||||
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case Qt::DisplayRole:{
|
case Qt::DisplayRole:{
|
||||||
QString text;
|
QString text;
|
||||||
if (!info->friendly_name_.isEmpty()) {
|
if (!device_info->friendly_name_.isEmpty()) {
|
||||||
text = info->friendly_name_;
|
text = device_info->friendly_name_;
|
||||||
}
|
}
|
||||||
else if (info->BestBackend()) {
|
else if (device_info->BestBackend()) {
|
||||||
text = info->BestBackend()->unique_id_;
|
text = device_info->BestBackend()->unique_id_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info->size_ > 0) {
|
if (device_info->size_ > 0) {
|
||||||
text = text + QStringLiteral(" (%1)").arg(Utilities::PrettySize(info->size_));
|
text = text + QStringLiteral(" (%1)").arg(Utilities::PrettySize(device_info->size_));
|
||||||
}
|
}
|
||||||
if (info->device_) info->device_->Refresh();
|
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
case Qt::DecorationRole:{
|
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
|
// Disconnected but remembered
|
||||||
QPainter p(&pixmap);
|
QPainter p(&pixmap);
|
||||||
p.drawPixmap(kDeviceIconSize - kDeviceIconOverlaySize, kDeviceIconSize - kDeviceIconOverlaySize, not_connected_overlay_.pixmap(kDeviceIconOverlaySize));
|
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:
|
case Role_FriendlyName:
|
||||||
return info->friendly_name_;
|
return device_info->friendly_name_;
|
||||||
|
|
||||||
case Role_UniqueId:
|
case Role_UniqueId:
|
||||||
if (!info->BestBackend()) return QString();
|
if (!device_info->BestBackend()) return QString();
|
||||||
return info->BestBackend()->unique_id_;
|
return device_info->BestBackend()->unique_id_;
|
||||||
|
|
||||||
case Role_IconName:
|
case Role_IconName:
|
||||||
return info->icon_name_;
|
return device_info->icon_name_;
|
||||||
|
|
||||||
case Role_Capacity:
|
case Role_Capacity:
|
||||||
case MusicStorage::Role_Capacity:
|
case MusicStorage::Role_Capacity:
|
||||||
return info->size_;
|
return device_info->size_;
|
||||||
|
|
||||||
case Role_FreeSpace:
|
case Role_FreeSpace:
|
||||||
case MusicStorage::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:
|
case Role_State:
|
||||||
if (info->device_) return State_Connected;
|
if (device_info->device_) return QVariant::fromValue(State::Connected);
|
||||||
if (info->BestBackend() && info->BestBackend()->lister_) {
|
if (device_info->BestBackend() && device_info->BestBackend()->lister_) {
|
||||||
if (info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) return State_NotMounted;
|
if (device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) return QVariant::fromValue(State::NotMounted);
|
||||||
return State_NotConnected;
|
return QVariant::fromValue(State::NotConnected);
|
||||||
}
|
}
|
||||||
return State_Remembered;
|
return QVariant::fromValue(State::Remembered);
|
||||||
|
|
||||||
case Role_UpdatingPercentage:
|
case Role_UpdatingPercentage:
|
||||||
if (info->task_percentage_ == -1) return QVariant();
|
if (device_info->task_percentage_ == -1) return QVariant();
|
||||||
return info->task_percentage_;
|
return device_info->task_percentage_;
|
||||||
|
|
||||||
case MusicStorage::Role_Storage:
|
case MusicStorage::Role_Storage:
|
||||||
if (!info->device_ && info->database_id_ != -1) {
|
if (!device_info->device_ && device_info->database_id_ != -1) {
|
||||||
const_cast<DeviceManager*>(this)->Connect(info);
|
const_cast<DeviceManager*>(this)->Connect(device_info);
|
||||||
}
|
}
|
||||||
if (!info->device_) return QVariant();
|
if (!device_info->device_) return QVariant();
|
||||||
return QVariant::fromValue<SharedPtr<MusicStorage>>(info->device_);
|
return QVariant::fromValue<SharedPtr<MusicStorage>>(device_info->device_);
|
||||||
|
|
||||||
case MusicStorage::Role_StorageForceConnect:
|
case MusicStorage::Role_StorageForceConnect:
|
||||||
if (!info->BestBackend()) return QVariant();
|
if (!device_info->BestBackend()) return QVariant();
|
||||||
if (!info->device_) {
|
if (!device_info->device_) {
|
||||||
if (info->database_id_ == -1 && !info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) {
|
if (device_info->database_id_ == -1 && !device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) {
|
||||||
if (info->BestBackend()->lister_->AskForScan(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));
|
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);
|
QPushButton *pushbutton = dialog->addButton(tr("Connect device"), QMessageBox::AcceptRole);
|
||||||
dialog->exec();
|
dialog->exec();
|
||||||
if (dialog->clickedButton() != pushbutton) return QVariant();
|
if (dialog->clickedButton() != pushbutton) return QVariant();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const_cast<DeviceManager*>(this)->Connect(info);
|
const_cast<DeviceManager*>(this)->Connect(device_info);
|
||||||
}
|
}
|
||||||
if (!info->device_) return QVariant();
|
if (!device_info->device_) return QVariant();
|
||||||
return QVariant::fromValue<SharedPtr<MusicStorage>>(info->device_);
|
return QVariant::fromValue<SharedPtr<MusicStorage>>(device_info->device_);
|
||||||
|
|
||||||
case Role_MountPath:{
|
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
|
#ifdef Q_OS_WIN32
|
||||||
if (ret.startsWith(u'/')) ret.remove(0, 1);
|
if (ret.startsWith(u'/')) ret.remove(0, 1);
|
||||||
#endif
|
#endif
|
||||||
@@ -376,17 +371,17 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case Role_TranscodeMode:
|
case Role_TranscodeMode:
|
||||||
return static_cast<int>(info->transcode_mode_);
|
return static_cast<int>(device_info->transcode_mode_);
|
||||||
|
|
||||||
case Role_TranscodeFormat:
|
case Role_TranscodeFormat:
|
||||||
return static_cast<int>(info->transcode_format_);
|
return static_cast<int>(device_info->transcode_format_);
|
||||||
|
|
||||||
case Role_SongCount:
|
case Role_SongCount:
|
||||||
if (!info->device_) return QVariant();
|
if (!device_info->device_) return QVariant();
|
||||||
return info->device_->song_count();
|
return device_info->device_->song_count();
|
||||||
|
|
||||||
case Role_CopyMusic:
|
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;
|
else return false;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -410,7 +405,9 @@ DeviceInfo *DeviceManager::FindDeviceById(const QString &id) const {
|
|||||||
|
|
||||||
for (int i = 0; i < devices_.count(); ++i) {
|
for (int i = 0; i < devices_.count(); ++i) {
|
||||||
for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) {
|
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 (int i = 0; i < devices_.count(); ++i) {
|
||||||
for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) {
|
for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) {
|
||||||
if (!backend.lister_) continue;
|
if (!backend.lister_) continue;
|
||||||
|
|
||||||
const QList<QUrl> device_urls = backend.lister_->MakeDeviceUrls(backend.unique_id_);
|
const QList<QUrl> device_urls = backend.lister_->MakeDeviceUrls(backend.unique_id_);
|
||||||
for (const QUrl &url : device_urls) {
|
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_)) {
|
for (const QString &unique_id : unique_ids) {
|
||||||
DeviceInfo *match = FindDeviceById(backend.unique_id_);
|
DeviceInfo *device_info_match = FindDeviceById(unique_id);
|
||||||
if (match) return match;
|
if (device_info_match) {
|
||||||
|
return device_info_match;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -455,42 +456,42 @@ void DeviceManager::PhysicalDeviceAdded(const QString &id) {
|
|||||||
qLog(Info) << "Device added:" << id << lister->DeviceUniqueIDs();
|
qLog(Info) << "Device added:" << id << lister->DeviceUniqueIDs();
|
||||||
|
|
||||||
// Do we have this device already?
|
// Do we have this device already?
|
||||||
DeviceInfo *info = FindDeviceById(id);
|
DeviceInfo *device_info = FindDeviceById(id);
|
||||||
if (info) {
|
if (device_info) {
|
||||||
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) {
|
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
|
||||||
if (info->backends_[backend_index].unique_id_ == id) {
|
if (device_info->backends_[backend_index].unique_id_ == id) {
|
||||||
info->backends_[backend_index].lister_ = lister;
|
device_info->backends_[backend_index].lister_ = lister;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QModelIndex idx = ItemToIndex(info);
|
QModelIndex idx = ItemToIndex(device_info);
|
||||||
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
|
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Check if we have another device with the same URL
|
// Check if we have another device with the same URL
|
||||||
info = FindDeviceByUrl(lister->MakeDeviceUrls(id));
|
device_info = FindDeviceByUrl(lister->MakeDeviceUrls(id));
|
||||||
if (info) {
|
if (device_info) {
|
||||||
// Add this device's lister to the existing device
|
// 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 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) {
|
if (device_info->database_id_ == -1 && device_info->BestBackend() && device_info->BestBackend()->lister_ == lister) {
|
||||||
info->friendly_name_ = lister->MakeFriendlyName(id);
|
device_info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||||
info->size_ = lister->DeviceCapacity(id);
|
device_info->size_ = lister->DeviceCapacity(id);
|
||||||
info->SetIcon(lister->DeviceIcons(id), info->friendly_name_);
|
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);
|
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// It's a completely new device
|
// It's a completely new device
|
||||||
info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
device_info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||||
info->backends_ << DeviceInfo::Backend(lister, id);
|
device_info->backends_ << DeviceInfo::Backend(lister, id);
|
||||||
info->friendly_name_ = lister->MakeFriendlyName(id);
|
device_info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||||
info->size_ = lister->DeviceCapacity(id);
|
device_info->size_ = lister->DeviceCapacity(id);
|
||||||
info->SetIcon(lister->DeviceIcons(id), info->friendly_name_);
|
device_info->LoadIcon(lister->DeviceIcons(id), device_info->friendly_name_);
|
||||||
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
|
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
|
||||||
devices_ << info;
|
devices_ << device_info;
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -503,42 +504,42 @@ void DeviceManager::PhysicalDeviceRemoved(const QString &id) {
|
|||||||
|
|
||||||
qLog(Info) << "Device removed:" << id;
|
qLog(Info) << "Device removed:" << id;
|
||||||
|
|
||||||
DeviceInfo *info = FindDeviceById(id);
|
DeviceInfo *device_info = FindDeviceById(id);
|
||||||
if (!info) return;
|
if (!device_info) return;
|
||||||
|
|
||||||
QModelIndex idx = ItemToIndex(info);
|
const QModelIndex idx = ItemToIndex(device_info);
|
||||||
if (!idx.isValid()) return;
|
if (!idx.isValid()) return;
|
||||||
|
|
||||||
if (info->database_id_ != -1) {
|
if (device_info->database_id_ != -1) {
|
||||||
// Keep the structure around, but just "disconnect" it
|
// Keep the structure around, but just "disconnect" it
|
||||||
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) {
|
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
|
||||||
if (info->backends_[backend_index].unique_id_ == id) {
|
if (device_info->backends_[backend_index].unique_id_ == id) {
|
||||||
info->backends_[backend_index].lister_ = nullptr;
|
device_info->backends_[backend_index].lister_ = nullptr;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info->device_ && info->device_->lister() == lister) {
|
if (device_info->device_ && device_info->device_->lister() == lister) {
|
||||||
info->device_->Close();
|
device_info->device_->Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info->device_) Q_EMIT DeviceDisconnected(idx);
|
if (!device_info->device_) Q_EMIT DeviceDisconnected(idx);
|
||||||
|
|
||||||
Q_EMIT dataChanged(idx, idx);
|
Q_EMIT dataChanged(idx, idx);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// If this was the last lister for the device then remove it from the model
|
// 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) {
|
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
|
||||||
if (info->backends_[backend_index].unique_id_ == id) {
|
if (device_info->backends_[backend_index].unique_id_ == id) {
|
||||||
info->backends_.removeAt(backend_index);
|
device_info->backends_.removeAt(backend_index);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info->backends_.isEmpty()) {
|
if (device_info->backends_.isEmpty()) {
|
||||||
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
|
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
|
||||||
devices_.removeAll(info);
|
devices_.removeAll(device_info);
|
||||||
root_->Delete(info->row);
|
root_->Delete(device_info->row);
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,8 +551,8 @@ void DeviceManager::PhysicalDeviceChanged(const QString &id) {
|
|||||||
DeviceLister *lister = qobject_cast<DeviceLister*>(sender());
|
DeviceLister *lister = qobject_cast<DeviceLister*>(sender());
|
||||||
Q_UNUSED(lister);
|
Q_UNUSED(lister);
|
||||||
|
|
||||||
DeviceInfo *info = FindDeviceById(id);
|
DeviceInfo *device_info = FindDeviceById(id);
|
||||||
if (!info) return;
|
if (!device_info) return;
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
|
|
||||||
@@ -561,40 +562,41 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(const QModelIndex &idx) {
|
|||||||
|
|
||||||
SharedPtr<ConnectedDevice> ret;
|
SharedPtr<ConnectedDevice> ret;
|
||||||
|
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
if (!info) return ret;
|
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 (!device_info) {
|
||||||
|
return SharedPtr<ConnectedDevice>();
|
||||||
if (!info) return ret;
|
|
||||||
if (info->device_) { // Already connected
|
|
||||||
return info->device_;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info->BestBackend() || !info->BestBackend()->lister_) { // Not physically connected
|
if (device_info->device_) { // Already connected
|
||||||
return ret;
|
return device_info->device_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) { // Mount the device
|
if (!device_info->BestBackend() || !device_info->BestBackend()->lister_) { // Not physically connected
|
||||||
info->BestBackend()->lister_->MountDeviceAsync(info->BestBackend()->unique_id_);
|
return SharedPtr<ConnectedDevice>();
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (first_time) {
|
||||||
// We haven't stored this device in the database before
|
// 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
|
// Get the device URLs
|
||||||
const QList<QUrl> urls = info->BestBackend()->lister_->MakeDeviceUrls(info->BestBackend()->unique_id_);
|
const QList<QUrl> urls = device_info->BestBackend()->lister_->MakeDeviceUrls(device_info->BestBackend()->unique_id_);
|
||||||
if (urls.isEmpty()) return ret;
|
if (urls.isEmpty()) return SharedPtr<ConnectedDevice>();
|
||||||
|
|
||||||
// Take the first URL that we have a handler for
|
// Take the first URL that we have a handler for
|
||||||
QUrl device_url;
|
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("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."),
|
tr("If you continue, this device will work slowly and songs copied to it may not work."),
|
||||||
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
|
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
|
||||||
return ret;
|
return SharedPtr<ConnectedDevice>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.scheme() == "ipod"_L1) {
|
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("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."),
|
tr("If you continue, this device will work slowly and songs copied to it may not work."),
|
||||||
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
|
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)));
|
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());
|
QMetaObject meta_object = device_classes_.value(device_url.scheme());
|
||||||
QObject *instance = meta_object.newInstance(
|
QObject *instance = meta_object.newInstance(
|
||||||
Q_ARG(QUrl, device_url),
|
Q_ARG(QUrl, device_url),
|
||||||
Q_ARG(DeviceLister*, info->BestBackend()->lister_),
|
Q_ARG(DeviceLister*, device_info->BestBackend()->lister_),
|
||||||
Q_ARG(QString, info->BestBackend()->unique_id_),
|
Q_ARG(QString, device_info->BestBackend()->unique_id_),
|
||||||
Q_ARG(DeviceManager*, this),
|
Q_ARG(DeviceManager*, this),
|
||||||
Q_ARG(SharedPtr<TaskManager>, task_manager_),
|
Q_ARG(SharedPtr<TaskManager>, task_manager_),
|
||||||
Q_ARG(SharedPtr<Database>, database_),
|
Q_ARG(SharedPtr<Database>, database_),
|
||||||
Q_ARG(SharedPtr<TagReaderClient>, tagreader_client_),
|
Q_ARG(SharedPtr<TagReaderClient>, tagreader_client_),
|
||||||
Q_ARG(SharedPtr<AlbumCoverLoader>, albumcover_loader_),
|
Q_ARG(SharedPtr<AlbumCoverLoader>, albumcover_loader_),
|
||||||
Q_ARG(int, info->database_id_),
|
Q_ARG(int, device_info->database_id_),
|
||||||
Q_ARG(bool, first_time));
|
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();
|
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) {
|
if (!result) {
|
||||||
qLog(Warning) << "Could not connect to device" << device_url.toString();
|
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);
|
QModelIndex idx = ItemToIndex(device_info);
|
||||||
if (!idx.isValid()) return ret;
|
if (!idx.isValid()) return connected_device;
|
||||||
|
|
||||||
Q_EMIT dataChanged(idx, idx);
|
Q_EMIT dataChanged(idx, idx);
|
||||||
|
|
||||||
QObject::connect(&*info->device_, &ConnectedDevice::TaskStarted, this, &DeviceManager::DeviceTaskStarted);
|
QObject::connect(&*device_info->device_, &ConnectedDevice::TaskStarted, this, &DeviceManager::DeviceTaskStarted);
|
||||||
QObject::connect(&*info->device_, &ConnectedDevice::SongCountUpdated, this, &DeviceManager::DeviceSongCountUpdated);
|
QObject::connect(&*device_info->device_, &ConnectedDevice::SongCountUpdated, this, &DeviceManager::DeviceSongCountUpdated);
|
||||||
QObject::connect(&*info->device_, &ConnectedDevice::DeviceConnectFinished, this, &DeviceManager::DeviceConnectFinished);
|
QObject::connect(&*device_info->device_, &ConnectedDevice::DeviceConnectFinished, this, &DeviceManager::DeviceConnectFinished);
|
||||||
QObject::connect(&*info->device_, &ConnectedDevice::DeviceCloseFinished, this, &DeviceManager::DeviceCloseFinished);
|
QObject::connect(&*device_info->device_, &ConnectedDevice::DeviceCloseFinished, this, &DeviceManager::DeviceCloseFinished);
|
||||||
QObject::connect(&*info->device_, &ConnectedDevice::Error, this, &DeviceManager::DeviceError);
|
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) {
|
void DeviceManager::DeviceConnectFinished(const QString &id, const bool success) {
|
||||||
|
|
||||||
DeviceInfo *info = FindDeviceById(id);
|
DeviceInfo *device_info = FindDeviceById(id);
|
||||||
if (!info) return;
|
if (!device_info) return;
|
||||||
|
|
||||||
QModelIndex idx = ItemToIndex(info);
|
QModelIndex idx = ItemToIndex(device_info);
|
||||||
if (!idx.isValid()) return;
|
if (!idx.isValid()) return;
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
Q_EMIT DeviceConnected(idx);
|
Q_EMIT DeviceConnected(idx);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
info->device_->Close();
|
device_info->device_->Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeviceManager::DeviceCloseFinished(const QString &id) {
|
void DeviceManager::DeviceCloseFinished(const QString &id) {
|
||||||
|
|
||||||
DeviceInfo *info = FindDeviceById(id);
|
DeviceInfo *device_info = FindDeviceById(id);
|
||||||
if (!info) return;
|
if (!device_info) return;
|
||||||
|
|
||||||
info->device_.reset();
|
device_info->device_.reset();
|
||||||
|
|
||||||
QModelIndex idx = ItemToIndex(info);
|
QModelIndex idx = ItemToIndex(device_info);
|
||||||
if (!idx.isValid()) return;
|
if (!idx.isValid()) return;
|
||||||
|
|
||||||
Q_EMIT DeviceDisconnected(idx);
|
Q_EMIT DeviceDisconnected(idx);
|
||||||
Q_EMIT dataChanged(idx, idx);
|
Q_EMIT dataChanged(idx, idx);
|
||||||
|
|
||||||
if (info->unmount_ && info->BestBackend() && info->BestBackend()->lister_) {
|
if (device_info->unmount_ && device_info->BestBackend() && device_info->BestBackend()->lister_) {
|
||||||
info->BestBackend()->lister_->UnmountDeviceAsync(info->BestBackend()->unique_id_);
|
device_info->BestBackend()->lister_->UnmountDeviceAsync(device_info->BestBackend()->unique_id_);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info->forget_) {
|
if (device_info->forget_) {
|
||||||
RemoveFromDB(info, idx);
|
RemoveFromDB(device_info, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceInfo *DeviceManager::GetDevice(const QModelIndex &idx) const {
|
DeviceInfo *DeviceManager::GetDevice(const QModelIndex &idx) const {
|
||||||
|
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
return info;
|
return device_info;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(const QModelIndex &idx) const {
|
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(const QModelIndex &idx) const {
|
||||||
|
|
||||||
SharedPtr<ConnectedDevice> ret;
|
SharedPtr<ConnectedDevice> connected_device;
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
if (!info) return ret;
|
if (!device_info) return connected_device;
|
||||||
return info->device_;
|
return device_info->device_;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(DeviceInfo *info) const {
|
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(DeviceInfo *device_info) const {
|
||||||
|
|
||||||
SharedPtr<ConnectedDevice> ret;
|
SharedPtr<ConnectedDevice> connected_device;
|
||||||
if (!info) return ret;
|
if (!device_info) return connected_device;
|
||||||
return info->device_;
|
return device_info->device_;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,9 +752,9 @@ int DeviceManager::GetDatabaseId(const QModelIndex &idx) const {
|
|||||||
|
|
||||||
if (!idx.isValid()) return -1;
|
if (!idx.isValid()) return -1;
|
||||||
|
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
if (!info) return -1;
|
if (!device_info) return -1;
|
||||||
return info->database_id_;
|
return device_info->database_id_;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,17 +762,17 @@ DeviceLister *DeviceManager::GetLister(const QModelIndex &idx) const {
|
|||||||
|
|
||||||
if (!idx.isValid()) return nullptr;
|
if (!idx.isValid()) return nullptr;
|
||||||
|
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
if (!info || !info->BestBackend()) return nullptr;
|
if (!device_info || !device_info->BestBackend()) return nullptr;
|
||||||
return info->BestBackend()->lister_;
|
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);
|
Q_UNUSED(idx);
|
||||||
|
|
||||||
info->device_->Close();
|
device_info->device_->Close();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,37 +780,37 @@ void DeviceManager::Forget(const QModelIndex &idx) {
|
|||||||
|
|
||||||
if (!idx.isValid()) return;
|
if (!idx.isValid()) return;
|
||||||
|
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
if (!info) return;
|
if (!device_info) return;
|
||||||
|
|
||||||
if (info->database_id_ == -1) return;
|
if (device_info->database_id_ == -1) return;
|
||||||
|
|
||||||
if (info->device_) {
|
if (device_info->device_) {
|
||||||
info->forget_ = true;
|
device_info->forget_ = true;
|
||||||
Disconnect(info, idx);
|
Disconnect(device_info, idx);
|
||||||
}
|
}
|
||||||
else {
|
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_);
|
backend_->RemoveDevice(device_info->database_id_);
|
||||||
info->database_id_ = -1;
|
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());
|
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
|
||||||
devices_.removeAll(info);
|
devices_.removeAll(device_info);
|
||||||
root_->Delete(info->row);
|
root_->Delete(device_info->row);
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
}
|
}
|
||||||
else { // It's still attached, set the name and icon back to what they were originally
|
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);
|
device_info->friendly_name_ = device_info->BestBackend()->lister_->MakeFriendlyName(id);
|
||||||
info->SetIcon(info->BestBackend()->lister_->DeviceIcons(id), info->friendly_name_);
|
device_info->LoadIcon(device_info->BestBackend()->lister_->DeviceIcons(id), device_info->friendly_name_);
|
||||||
Q_EMIT dataChanged(idx, idx);
|
Q_EMIT dataChanged(idx, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,18 +820,18 @@ void DeviceManager::SetDeviceOptions(const QModelIndex &idx, const QString &frie
|
|||||||
|
|
||||||
if (!idx.isValid()) return;
|
if (!idx.isValid()) return;
|
||||||
|
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
if (!info) return;
|
if (!device_info) return;
|
||||||
|
|
||||||
info->friendly_name_ = friendly_name;
|
device_info->friendly_name_ = friendly_name;
|
||||||
info->SetIcon(QVariantList() << icon_name, friendly_name);
|
device_info->LoadIcon(QVariantList() << icon_name, friendly_name);
|
||||||
info->transcode_mode_ = mode;
|
device_info->transcode_mode_ = mode;
|
||||||
info->transcode_format_ = format;
|
device_info->transcode_format_ = format;
|
||||||
|
|
||||||
Q_EMIT dataChanged(idx, idx);
|
Q_EMIT dataChanged(idx, idx);
|
||||||
|
|
||||||
if (info->database_id_ != -1) {
|
if (device_info->database_id_ != -1) {
|
||||||
backend_->SetDeviceOptions(info->database_id_, friendly_name, icon_name, mode, format);
|
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;
|
if (!device) return;
|
||||||
|
|
||||||
for (int i = 0; i < devices_.count(); ++i) {
|
for (int i = 0; i < devices_.count(); ++i) {
|
||||||
DeviceInfo *info = devices_.value(i);
|
DeviceInfo *device_info = devices_.value(i);
|
||||||
if (info->device_ && &*info->device_ == device) {
|
if (device_info->device_ && &*device_info->device_ == device) {
|
||||||
QModelIndex index = ItemToIndex(info);
|
QModelIndex index = ItemToIndex(device_info);
|
||||||
if (!index.isValid()) continue;
|
if (!index.isValid()) continue;
|
||||||
active_tasks_[id] = index;
|
active_tasks_[id] = index;
|
||||||
info->task_percentage_ = 0;
|
device_info->task_percentage_ = 0;
|
||||||
Q_EMIT dataChanged(index, index);
|
Q_EMIT dataChanged(index, index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -864,12 +866,12 @@ void DeviceManager::TasksChanged() {
|
|||||||
const QPersistentModelIndex idx = active_tasks_.value(task.id);
|
const QPersistentModelIndex idx = active_tasks_.value(task.id);
|
||||||
if (!idx.isValid()) continue;
|
if (!idx.isValid()) continue;
|
||||||
|
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
if (task.progress_max) {
|
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 {
|
else {
|
||||||
info->task_percentage_ = 0;
|
device_info->task_percentage_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_EMIT dataChanged(idx, idx);
|
Q_EMIT dataChanged(idx, idx);
|
||||||
@@ -881,10 +883,10 @@ void DeviceManager::TasksChanged() {
|
|||||||
|
|
||||||
if (!idx.isValid()) continue;
|
if (!idx.isValid()) continue;
|
||||||
|
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
if (!info) continue;
|
if (!device_info) continue;
|
||||||
|
|
||||||
info->task_percentage_ = -1;
|
device_info->task_percentage_ = -1;
|
||||||
Q_EMIT dataChanged(idx, idx);
|
Q_EMIT dataChanged(idx, idx);
|
||||||
|
|
||||||
active_tasks_.remove(active_tasks_.key(idx));
|
active_tasks_.remove(active_tasks_.key(idx));
|
||||||
@@ -900,17 +902,17 @@ void DeviceManager::Unmount(const QModelIndex &idx) {
|
|||||||
|
|
||||||
if (!idx.isValid()) return;
|
if (!idx.isValid()) return;
|
||||||
|
|
||||||
DeviceInfo *info = IndexToItem(idx);
|
DeviceInfo *device_info = IndexToItem(idx);
|
||||||
if (!info) return;
|
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_) {
|
if (device_info->device_) {
|
||||||
info->unmount_ = true;
|
device_info->unmount_ = true;
|
||||||
Disconnect(info, idx);
|
Disconnect(device_info, idx);
|
||||||
}
|
}
|
||||||
else if (info->BestBackend() && info->BestBackend()->lister_) {
|
else if (device_info->BestBackend() && device_info->BestBackend()->lister_) {
|
||||||
info->BestBackend()->lister_->UnmountDeviceAsync(info->BestBackend()->unique_id_);
|
device_info->BestBackend()->lister_->UnmountDeviceAsync(device_info->BestBackend()->unique_id_);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -919,13 +921,13 @@ void DeviceManager::DeviceSongCountUpdated(const int count) {
|
|||||||
|
|
||||||
Q_UNUSED(count);
|
Q_UNUSED(count);
|
||||||
|
|
||||||
ConnectedDevice *device = qobject_cast<ConnectedDevice*>(sender());
|
ConnectedDevice *connected_device = qobject_cast<ConnectedDevice*>(sender());
|
||||||
if (!device) return;
|
if (!connected_device) return;
|
||||||
|
|
||||||
DeviceInfo *info = FindDeviceById(device->unique_id());
|
DeviceInfo *device_info = FindDeviceById(connected_device->unique_id());
|
||||||
if (!info) return;
|
if (!device_info) return;
|
||||||
|
|
||||||
QModelIndex idx = ItemToIndex(info);
|
QModelIndex idx = ItemToIndex(device_info);
|
||||||
if (!idx.isValid()) return;
|
if (!idx.isValid()) return;
|
||||||
|
|
||||||
Q_EMIT dataChanged(idx, idx);
|
Q_EMIT dataChanged(idx, idx);
|
||||||
@@ -934,10 +936,10 @@ void DeviceManager::DeviceSongCountUpdated(const int count) {
|
|||||||
|
|
||||||
QString DeviceManager::DeviceNameByID(const QString &unique_id) {
|
QString DeviceManager::DeviceNameByID(const QString &unique_id) {
|
||||||
|
|
||||||
DeviceInfo *info = FindDeviceById(unique_id);
|
DeviceInfo *device_info = FindDeviceById(unique_id);
|
||||||
if (!info) return QString();
|
if (!device_info) return QString();
|
||||||
|
|
||||||
QModelIndex idx = ItemToIndex(info);
|
QModelIndex idx = ItemToIndex(device_info);
|
||||||
if (!idx.isValid()) return QString();
|
if (!idx.isValid()) return QString();
|
||||||
|
|
||||||
return data(idx, DeviceManager::Role_FriendlyName).toString();
|
return data(idx, DeviceManager::Role_FriendlyName).toString();
|
||||||
|
|||||||
@@ -85,11 +85,11 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
|||||||
LastRole,
|
LastRole,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum State {
|
enum class State {
|
||||||
State_Remembered,
|
Remembered,
|
||||||
State_NotMounted,
|
NotMounted,
|
||||||
State_NotConnected,
|
NotConnected,
|
||||||
State_Connected,
|
Connected,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const int kDeviceIconSize;
|
static const int kDeviceIconSize;
|
||||||
@@ -104,17 +104,17 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
|||||||
DeviceLister *GetLister(const QModelIndex &idx) const;
|
DeviceLister *GetLister(const QModelIndex &idx) const;
|
||||||
DeviceInfo *GetDevice(const QModelIndex &idx) const;
|
DeviceInfo *GetDevice(const QModelIndex &idx) const;
|
||||||
SharedPtr<ConnectedDevice> GetConnectedDevice(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 *FindDeviceById(const QString &id) const;
|
||||||
DeviceInfo *FindDeviceByUrl(const QList<QUrl> &url) const;
|
DeviceInfo *FindDeviceByUrl(const QList<QUrl> &url) const;
|
||||||
QString DeviceNameByID(const QString &unique_id);
|
QString DeviceNameByID(const QString &unique_id);
|
||||||
DeviceInfo *FindEquivalentDevice(DeviceInfo *info) const;
|
DeviceInfo *FindEquivalentDevice(const QStringList &unique_ids) const;
|
||||||
|
|
||||||
// Actions on devices
|
// Actions on devices
|
||||||
SharedPtr<ConnectedDevice> Connect(DeviceInfo *info);
|
SharedPtr<ConnectedDevice> Connect(DeviceInfo *device_info);
|
||||||
SharedPtr<ConnectedDevice> Connect(const QModelIndex &idx);
|
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 Forget(const QModelIndex &idx);
|
||||||
void UnmountAsync(const QModelIndex &idx);
|
void UnmountAsync(const QModelIndex &idx);
|
||||||
|
|
||||||
@@ -128,9 +128,10 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
|||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void ExitFinished();
|
void ExitFinished();
|
||||||
|
void DevicesLoaded(const DeviceDatabaseBackend::DeviceList &devices);
|
||||||
void DeviceConnected(const QModelIndex idx);
|
void DeviceConnected(const QModelIndex idx);
|
||||||
void DeviceDisconnected(const QModelIndex idx);
|
void DeviceDisconnected(const QModelIndex idx);
|
||||||
void DeviceCreatedFromDB(DeviceInfo *info);
|
void DeviceCreatedFromDB(DeviceInfo *device_info);
|
||||||
void DeviceError(const QString &error);
|
void DeviceError(const QString &error);
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
@@ -143,7 +144,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
|||||||
void LoadAllDevices();
|
void LoadAllDevices();
|
||||||
void DeviceConnectFinished(const QString &id, bool success);
|
void DeviceConnectFinished(const QString &id, bool success);
|
||||||
void DeviceCloseFinished(const QString &id);
|
void DeviceCloseFinished(const QString &id);
|
||||||
void AddDeviceFromDB(DeviceInfo *info);
|
void AddDevicesFromDB(const DeviceDatabaseBackend::DeviceList &devices);
|
||||||
void BackendClosed();
|
void BackendClosed();
|
||||||
void ListerClosed();
|
void ListerClosed();
|
||||||
void DeviceDestroyed();
|
void DeviceDestroyed();
|
||||||
@@ -154,7 +155,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
|||||||
|
|
||||||
DeviceDatabaseBackend::Device InfoToDatabaseDevice(const DeviceInfo &info) const;
|
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 CloseDevices();
|
||||||
void CloseListers();
|
void CloseListers();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
#include "devicemanager.h"
|
#include "devicemanager.h"
|
||||||
#include "devicestatefiltermodel.h"
|
#include "devicestatefiltermodel.h"
|
||||||
|
|
||||||
DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::State state)
|
DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, const DeviceManager::State state)
|
||||||
: QSortFilterProxyModel(parent),
|
: QSortFilterProxyModel(parent),
|
||||||
state_(state) {
|
state_(state) {
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::S
|
|||||||
|
|
||||||
bool DeviceStateFilterModel::filterAcceptsRow(const int row, const QModelIndex &parent) const {
|
bool DeviceStateFilterModel::filterAcceptsRow(const int row, const QModelIndex &parent) const {
|
||||||
Q_UNUSED(parent)
|
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) {
|
void DeviceStateFilterModel::ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last) {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class DeviceStateFilterModel : public QSortFilterProxyModel {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
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;
|
void setSourceModel(QAbstractItemModel *sourceModel) override;
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class DeviceStateFilterModel : public QSortFilterProxyModel {
|
|||||||
void ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last);
|
void ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
DeviceManager::State state_;
|
const DeviceManager::State state_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // DEVICESTATEFILTERMODEL_H
|
#endif // DEVICESTATEFILTERMODEL_H
|
||||||
|
|||||||
@@ -128,19 +128,19 @@ void DeviceItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case DeviceManager::State_Remembered:
|
case DeviceManager::State::Remembered:
|
||||||
status_text = tr("Not connected");
|
status_text = tr("Not connected");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DeviceManager::State_NotMounted:
|
case DeviceManager::State::NotMounted:
|
||||||
status_text = tr("Not mounted - double click to mount");
|
status_text = tr("Not mounted - double click to mount");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DeviceManager::State_NotConnected:
|
case DeviceManager::State::NotConnected:
|
||||||
status_text = tr("Double click to open");
|
status_text = tr("Double click to open");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DeviceManager::State_Connected:{
|
case DeviceManager::State::Connected:{
|
||||||
QVariant song_count = idx.data(DeviceManager::Role_SongCount);
|
QVariant song_count = idx.data(DeviceManager::Role_SongCount);
|
||||||
if (song_count.isValid()) {
|
if (song_count.isValid()) {
|
||||||
int count = song_count.toInt();
|
int count = song_count.toInt();
|
||||||
|
|||||||
@@ -409,5 +409,6 @@ bool GPodDevice::FinishDelete(bool success, QString &error_text) {
|
|||||||
bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
|
bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
|
||||||
*ret << Song::FileType::MP4;
|
*ret << Song::FileType::MP4;
|
||||||
*ret << Song::FileType::MPEG;
|
*ret << Song::FileType::MPEG;
|
||||||
|
*ret << Song::FileType::ALAC;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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);
|
void UpdateDeviceFreeSpace(const QString &id);
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
struct MTPDevice {
|
struct MTPDevice {
|
||||||
MTPDevice() : capacity(0), free_space(0) {}
|
MTPDevice() : capacity(0), free_space(0) {}
|
||||||
QString vendor;
|
QString vendor;
|
||||||
@@ -74,6 +75,7 @@ class MacOsDeviceLister : public DeviceLister {
|
|||||||
quint64 capacity;
|
quint64 capacity;
|
||||||
quint64 free_space;
|
quint64 free_space;
|
||||||
};
|
};
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
void ExitAsync();
|
void ExitAsync();
|
||||||
|
|
||||||
@@ -91,11 +93,12 @@ class MacOsDeviceLister : public DeviceLister {
|
|||||||
|
|
||||||
static void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context);
|
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);
|
void RemovedMTPDevice(const QString &serial);
|
||||||
|
|
||||||
quint64 GetFreeSpace(const QUrl &url);
|
quint64 GetFreeSpace(const QUrl &url);
|
||||||
quint64 GetCapacity(const QUrl &url);
|
quint64 GetCapacity(const QUrl &url);
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
bool IsCDDevice(const QString &serial) const;
|
bool IsCDDevice(const QString &serial) const;
|
||||||
|
|
||||||
@@ -103,18 +106,23 @@ class MacOsDeviceLister : public DeviceLister {
|
|||||||
CFRunLoopRef run_loop_;
|
CFRunLoopRef run_loop_;
|
||||||
|
|
||||||
QMap<QString, QString> current_devices_;
|
QMap<QString, QString> current_devices_;
|
||||||
|
#ifdef HAVE_MTP
|
||||||
QMap<QString, MTPDevice> mtp_devices_;
|
QMap<QString, MTPDevice> mtp_devices_;
|
||||||
|
#endif
|
||||||
QSet<QString> cd_devices_;
|
QSet<QString> cd_devices_;
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
QMutex libmtp_mutex_;
|
QMutex libmtp_mutex_;
|
||||||
|
|
||||||
static QSet<MTPDevice> sMTPDeviceList;
|
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) {
|
inline bool operator==(const MacOsDeviceLister::MTPDevice &a, const MacOsDeviceLister::MTPDevice &b) {
|
||||||
return (a.vendor_id == b.vendor_id) && (a.product_id == b.product_id);
|
return (a.vendor_id == b.vendor_id) && (a.product_id == b.product_id);
|
||||||
}
|
}
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
#endif // MACDEVICELISTER_H
|
#endif // MACDEVICELISTER_H
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* 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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -21,7 +21,9 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
#include <libmtp.h>
|
#ifdef HAVE_MTP
|
||||||
|
# include <libmtp.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <AvailabilityMacros.h>
|
#include <AvailabilityMacros.h>
|
||||||
#include <CoreFoundation/CFRunLoop.h>
|
#include <CoreFoundation/CFRunLoop.h>
|
||||||
@@ -41,12 +43,15 @@
|
|||||||
#include <QScopeGuard>
|
#include <QScopeGuard>
|
||||||
|
|
||||||
#include "macosdevicelister.h"
|
#include "macosdevicelister.h"
|
||||||
#include "mtpconnection.h"
|
|
||||||
#include "includes/scoped_cftyperef.h"
|
#include "includes/scoped_cftyperef.h"
|
||||||
#include "includes/scoped_nsobject.h"
|
#include "includes/scoped_nsobject.h"
|
||||||
#include "core/logging.h"
|
#include "core/logging.h"
|
||||||
#include "core/scoped_nsautorelease_pool.h"
|
#include "core/scoped_nsautorelease_pool.h"
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
|
# include "mtpconnection.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#import <AppKit/NSWorkspace.h>
|
#import <AppKit/NSWorkspace.h>
|
||||||
#import <Foundation/NSDictionary.h>
|
#import <Foundation/NSDictionary.h>
|
||||||
#import <Foundation/NSNotification.h>
|
#import <Foundation/NSNotification.h>
|
||||||
@@ -102,11 +107,15 @@ class ScopedIOObject {
|
|||||||
// Libgphoto2 MTP detection code:
|
// 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
|
// 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;
|
QSet<MacOsDeviceLister::MTPDevice> MacOsDeviceLister::sMTPDeviceList;
|
||||||
|
#endif
|
||||||
|
|
||||||
size_t qHash(const MacOsDeviceLister::MTPDevice &d) {
|
#ifdef HAVE_MTP
|
||||||
return qHash(d.vendor_id) ^ qHash(d.product_id);
|
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) {}
|
MacOsDeviceLister::MacOsDeviceLister(QObject *parent) : DeviceLister(parent) {}
|
||||||
|
|
||||||
@@ -116,6 +125,7 @@ bool MacOsDeviceLister::Init() {
|
|||||||
|
|
||||||
ScopedNSAutoreleasePool pool;
|
ScopedNSAutoreleasePool pool;
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
// Populate MTP Device list.
|
// Populate MTP Device list.
|
||||||
if (sMTPDeviceList.empty()) {
|
if (sMTPDeviceList.empty()) {
|
||||||
LIBMTP_device_entry_t *devices = nullptr;
|
LIBMTP_device_entry_t *devices = nullptr;
|
||||||
@@ -126,25 +136,26 @@ bool MacOsDeviceLister::Init() {
|
|||||||
else {
|
else {
|
||||||
for (int i = 0; i < num; ++i) {
|
for (int i = 0; i < num; ++i) {
|
||||||
LIBMTP_device_entry_t device = devices[i];
|
LIBMTP_device_entry_t device = devices[i];
|
||||||
MTPDevice d;
|
MTPDevice mtp_device;
|
||||||
d.vendor = QString::fromLatin1(device.vendor);
|
mtp_device.vendor = QString::fromLatin1(device.vendor);
|
||||||
d.vendor_id = device.vendor_id;
|
mtp_device.vendor_id = device.vendor_id;
|
||||||
d.product = QString::fromLatin1(device.product);
|
mtp_device.product = QString::fromLatin1(device.product);
|
||||||
d.product_id = device.product_id;
|
mtp_device.product_id = device.product_id;
|
||||||
d.quirks = device.device_flags;
|
mtp_device.quirks = device.device_flags;
|
||||||
sMTPDeviceList << d;
|
sMTPDeviceList << mtp_device;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MTPDevice d;
|
MTPDevice mtp_device;
|
||||||
d.vendor = "SanDisk"_L1;
|
mtp_device.vendor = "SanDisk"_L1;
|
||||||
d.vendor_id = 0x781;
|
mtp_device.vendor_id = 0x781;
|
||||||
d.product = "Sansa Clip+"_L1;
|
mtp_device.product = "Sansa Clip+"_L1;
|
||||||
d.product_id = 0x74d0;
|
mtp_device.product_id = 0x74d0;
|
||||||
|
|
||||||
d.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
|
mtp_device.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
|
||||||
sMTPDeviceList << d;
|
sMTPDeviceList << mtp_device;
|
||||||
}
|
}
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
run_loop_ = CFRunLoopGetCurrent();
|
run_loop_ = CFRunLoopGetCurrent();
|
||||||
|
|
||||||
@@ -240,12 +251,14 @@ CFTypeRef GetUSBRegistryEntry(io_object_t device, CFStringRef key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) {
|
QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) {
|
||||||
|
|
||||||
ScopedCFTypeRef<CFStringRef> registry_string(reinterpret_cast<CFStringRef>(GetUSBRegistryEntry(device, key)));
|
ScopedCFTypeRef<CFStringRef> registry_string(reinterpret_cast<CFStringRef>(GetUSBRegistryEntry(device, key)));
|
||||||
if (registry_string) {
|
if (registry_string) {
|
||||||
return QString::fromUtf8([reinterpret_cast<NSString*>(registry_string.get()) UTF8String]);
|
return QString::fromUtf8([reinterpret_cast<NSString*>(registry_string.get()) UTF8String]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return QString();
|
return QString();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
|
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) {
|
int GetUSBDeviceClass(io_object_t device) {
|
||||||
|
|
||||||
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(
|
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(device, kIOServicePlane, CFSTR(kUSBInterfaceClass), kCFAllocatorDefault, kIORegistryIterateRecursively));
|
||||||
device,
|
|
||||||
kIOServicePlane,
|
|
||||||
CFSTR(kUSBInterfaceClass),
|
|
||||||
kCFAllocatorDefault,
|
|
||||||
kIORegistryIterateRecursively));
|
|
||||||
NSNumber *number = reinterpret_cast<NSNumber*>(interface_class.get());
|
NSNumber *number = reinterpret_cast<NSNumber*>(interface_class.get());
|
||||||
if (number) {
|
if (number) {
|
||||||
int ret = [number unsignedShortValue];
|
int ret = [number unsignedShortValue];
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -322,12 +331,14 @@ QString GetSerialForDevice(io_object_t device) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
QString GetSerialForMTPDevice(io_object_t device) {
|
QString GetSerialForMTPDevice(io_object_t device) {
|
||||||
|
|
||||||
scoped_nsobject<NSString> serial(reinterpret_cast<NSString*>(GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString))));
|
scoped_nsobject<NSString> serial(reinterpret_cast<NSString*>(GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString))));
|
||||||
return "MTP/"_L1 + QString::fromUtf8([serial UTF8String]);
|
return "MTP/"_L1 + QString::fromUtf8([serial UTF8String]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
|
QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
|
||||||
|
|
||||||
@@ -343,6 +354,7 @@ QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
quint64 MacOsDeviceLister::GetFreeSpace(const QUrl &url) {
|
quint64 MacOsDeviceLister::GetFreeSpace(const QUrl &url) {
|
||||||
|
|
||||||
QMutexLocker l(&libmtp_mutex_);
|
QMutexLocker l(&libmtp_mutex_);
|
||||||
@@ -380,6 +392,8 @@ quint64 MacOsDeviceLister::GetCapacity(const QUrl &url) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||||
|
|
||||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(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)];
|
NSString *kind = [properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionMediaKindKey)];
|
||||||
if (kind && strcmp([kind UTF8String], kIOCDMediaClass) == 0) {
|
if (kind && strcmp([kind UTF8String], kIOCDMediaClass) == 0) {
|
||||||
// CD inserted.
|
// CD inserted.
|
||||||
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||||
me->cd_devices_ << bsd_name;
|
me->cd_devices_ << bsd_name;
|
||||||
Q_EMIT me->DeviceAdded(bsd_name);
|
Q_EMIT me->DeviceAdded(bsd_name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif // HAVE_AUDIOCD
|
||||||
|
|
||||||
NSURL *volume_path = [[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy];
|
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));
|
ScopedIOObject device(DADiskCopyIOMedia(disk));
|
||||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(device.get()));
|
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(device.get()));
|
||||||
if (class_name && CFStringCompare(class_name.get(), CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) {
|
if (class_name && CFStringCompare(class_name.get(), CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) {
|
||||||
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||||
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||||
|
|
||||||
CFMutableDictionaryRef cf_properties;
|
CFMutableDictionaryRef cf_properties;
|
||||||
kern_return_t ret = IORegistryEntryCreateCFProperties(device.get(), &cf_properties, kCFAllocatorDefault, 0);
|
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) {
|
if (ret == KERN_SUCCESS) {
|
||||||
scoped_nsobject<NSDictionary> dict(reinterpret_cast<NSDictionary*>(cf_properties)); // Takes ownership.
|
scoped_nsobject<NSDictionary> dict(reinterpret_cast<NSDictionary*>(cf_properties)); // Takes ownership.
|
||||||
if ([[dict objectForKey:@"Removable"] intValue] == 1) {
|
if ([[dict objectForKey:@"Removable"] intValue] == 1) {
|
||||||
QString serial = GetSerialForDevice(device.get());
|
const QString serial = GetSerialForDevice(device.get());
|
||||||
if (!serial.isEmpty()) {
|
if (!serial.isEmpty()) {
|
||||||
me->current_devices_[serial] = QString::fromLatin1(DADiskGetBSDName(disk));
|
me->current_devices_[serial] = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||||
Q_EMIT me->DeviceAdded(serial);
|
Q_EMIT me->DeviceAdded(serial);
|
||||||
@@ -427,10 +441,9 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
|||||||
void MacOsDeviceLister::DiskRemovedCallback(DADiskRef disk, void *context) {
|
void MacOsDeviceLister::DiskRemovedCallback(DADiskRef disk, void *context) {
|
||||||
|
|
||||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
|
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
|
||||||
// We cannot access the USB tree when the disk is removed but we still get
|
// We cannot access the USB tree when the disk is removed but we still get the BSD disk name.
|
||||||
// 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)) {
|
if (me->cd_devices_.remove(bsd_name)) {
|
||||||
Q_EMIT me->DeviceRemoved(bsd_name);
|
Q_EMIT me->DeviceRemoved(bsd_name);
|
||||||
return;
|
return;
|
||||||
@@ -496,6 +509,7 @@ int GetBusNumber(io_object_t o) {
|
|||||||
void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||||
|
|
||||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
|
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
|
||||||
|
Q_UNUSED(me)
|
||||||
|
|
||||||
io_object_t object;
|
io_object_t object;
|
||||||
while ((object = IOIteratorNext(it))) {
|
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); });
|
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
|
||||||
|
|
||||||
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
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 *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
|
||||||
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
|
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
|
||||||
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
|
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
|
||||||
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
|
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;
|
MTPDevice mtp_device;
|
||||||
device.vendor = QString::fromUtf8([vendor UTF8String]);
|
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||||
device.product = QString::fromUtf8([product UTF8String]);
|
mtp_device.product = QString::fromUtf8([product UTF8String]);
|
||||||
device.vendor_id = [vendor_id unsignedShortValue];
|
mtp_device.vendor_id = [vendor_id unsignedShortValue];
|
||||||
device.product_id = [product_id unsignedShortValue];
|
mtp_device.product_id = [product_id unsignedShortValue];
|
||||||
device.quirks = 0;
|
mtp_device.quirks = 0;
|
||||||
|
|
||||||
device.bus = -1;
|
mtp_device.bus = -1;
|
||||||
device.address = -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.
|
// 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
|
// 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.
|
// Skip HID devices, printers and hubs.
|
||||||
interface_class == kUSBHIDInterfaceClass ||
|
interface_class == kUSBHIDInterfaceClass ||
|
||||||
interface_class == kUSBPrintingInterfaceClass ||
|
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")));
|
NSNumber *addr = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR("USB Address")));
|
||||||
int bus = GetBusNumber(object);
|
const int bus = GetBusNumber(object);
|
||||||
if (!addr || bus == -1) {
|
if (!addr || bus == -1) {
|
||||||
// Failed to get bus or address number.
|
// Failed to get bus or address number.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
device.bus = bus;
|
mtp_device.bus = bus;
|
||||||
device.address = [addr intValue];
|
mtp_device.address = [addr intValue];
|
||||||
|
|
||||||
// First check the libmtp device list.
|
// 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()) {
|
if (it2 != sMTPDeviceList.end()) {
|
||||||
// Fill in quirks flags from libmtp.
|
// Fill in quirks flags from libmtp.
|
||||||
device.quirks = it2->quirks;
|
mtp_device.quirks = it2->quirks;
|
||||||
me->FoundMTPDevice(device, GetSerialForMTPDevice(object));
|
me->FoundMTPDevice(mtp_device, GetSerialForMTPDevice(object));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
IOCFPlugInInterface **plugin_interface = nullptr;
|
IOCFPlugInInterface **plugin_interface = nullptr;
|
||||||
SInt32 score;
|
SInt32 score;
|
||||||
kern_return_t err = IOCreatePlugInInterfaceForService(
|
kern_return_t err = IOCreatePlugInInterfaceForService(object, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin_interface, &score);
|
||||||
object,
|
|
||||||
kIOUSBDeviceUserClientTypeID,
|
|
||||||
kIOCFPlugInInterfaceID,
|
|
||||||
&plugin_interface,
|
|
||||||
&score);
|
|
||||||
if (err != KERN_SUCCESS) {
|
if (err != KERN_SUCCESS) {
|
||||||
continue;
|
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);
|
bool ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, 2, &data);
|
||||||
if (!ret) continue;
|
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);
|
ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, string_len, &data);
|
||||||
if (!ret) continue;
|
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?).
|
// 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);
|
QString str = QString::fromUtf16(reinterpret_cast<char16_t*>(data.data() + 2), (data.size() / 2) - 2);
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
if (str.startsWith("MSFT100"_L1)) {
|
if (str.startsWith("MSFT100"_L1)) {
|
||||||
// We got the OS descriptor!
|
// We got the OS descriptor!
|
||||||
char vendor_code = data[16];
|
char vendor_code = data[16];
|
||||||
@@ -621,8 +637,10 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Hurray! We made it!
|
// 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) {
|
void MacOsDeviceLister::USBDeviceRemovedCallback(void *refcon, io_iterator_t it) {
|
||||||
|
|
||||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
|
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
|
||||||
|
Q_UNUSED(me)
|
||||||
|
|
||||||
io_object_t object;
|
io_object_t object;
|
||||||
while ((object = IOIteratorNext(it))) {
|
while ((object = IOIteratorNext(it))) {
|
||||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object));
|
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object));
|
||||||
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
|
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
|
||||||
|
|
||||||
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
|
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
|
||||||
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
|
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
|
||||||
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
|
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
|
||||||
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
|
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
|
||||||
QString serial = GetSerialForMTPDevice(object);
|
|
||||||
|
|
||||||
MTPDevice device;
|
const QString serial = GetSerialForMTPDevice(object);
|
||||||
device.vendor = QString::fromUtf8([vendor UTF8String]);
|
|
||||||
device.product = QString::fromUtf8([product UTF8String]);
|
MTPDevice mtp_device;
|
||||||
device.vendor_id = [vendor_id unsignedShortValue];
|
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||||
device.product_id = [product_id unsignedShortValue];
|
mtp_device.product = QString::fromUtf8([product UTF8String]);
|
||||||
|
mtp_device.vendor_id = [vendor_id unsignedShortValue];
|
||||||
|
mtp_device.product_id = [product_id unsignedShortValue];
|
||||||
|
|
||||||
me->RemovedMTPDevice(serial);
|
me->RemovedMTPDevice(serial);
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
|
|
||||||
void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
|
void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
|
||||||
|
|
||||||
int count = mtp_devices_.remove(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) {
|
void MacOsDeviceLister::FoundMTPDevice(const MTPDevice &device, const QString &serial) {
|
||||||
|
|
||||||
qLog(Debug) << "New MTP device detected!" << device.bus << device.address;
|
qLog(Debug) << "New MTP device detected!" << device.bus << device.address;
|
||||||
|
|
||||||
mtp_devices_[serial] = device;
|
mtp_devices_[serial] = device;
|
||||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
const QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||||
MTPDevice *d = &mtp_devices_[serial];
|
MTPDevice *mtp_device = &mtp_devices_[serial];
|
||||||
d->capacity = GetCapacity(urls[0]);
|
mtp_device->capacity = GetCapacity(urls[0]);
|
||||||
d->free_space = GetFreeSpace(urls[0]);
|
mtp_device->free_space = GetFreeSpace(urls[0]);
|
||||||
|
|
||||||
Q_EMIT DeviceAdded(serial);
|
Q_EMIT DeviceAdded(serial);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsMTPSerial(const QString &serial) { return serial.startsWith("MTP"_L1); }
|
bool IsMTPSerial(const QString &serial) { return serial.startsWith("MTP"_L1); }
|
||||||
|
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
bool MacOsDeviceLister::IsCDDevice(const QString &serial) const {
|
bool MacOsDeviceLister::IsCDDevice(const QString &serial) const {
|
||||||
return cd_devices_.contains(serial);
|
return cd_devices_.contains(serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
|
QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
if (IsMTPSerial(serial)) {
|
if (IsMTPSerial(serial)) {
|
||||||
const MTPDevice &device = mtp_devices_[serial];
|
const MTPDevice &mtp_device = mtp_devices_[serial];
|
||||||
if (device.vendor.isEmpty()) {
|
if (mtp_device.vendor.isEmpty()) {
|
||||||
return device.product;
|
return mtp_device.product;
|
||||||
}
|
}
|
||||||
else {
|
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<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
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));
|
ScopedIOObject device(DADiskCopyIOMedia(disk));
|
||||||
|
|
||||||
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||||
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||||
|
|
||||||
if (vendor.isEmpty()) {
|
if (vendor.isEmpty()) {
|
||||||
return product;
|
return product;
|
||||||
}
|
}
|
||||||
|
|
||||||
return vendor + QLatin1Char(' ') + product;
|
return vendor + QLatin1Char(' ') + product;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QUrl> MacOsDeviceLister::MakeDeviceUrls(const QString &serial) {
|
QList<QUrl> MacOsDeviceLister::MakeDeviceUrls(const QString &serial) {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
if (IsMTPSerial(serial)) {
|
if (IsMTPSerial(serial)) {
|
||||||
const MTPDevice &device = mtp_devices_[serial];
|
const MTPDevice &mtp_device = mtp_devices_[serial];
|
||||||
QString str = QString::asprintf("gphoto2://usb-%d-%d/", device.bus, device.address);
|
const QString str = QString::asprintf("gphoto2://usb-%d-%d/", mtp_device.bus, mtp_device.address);
|
||||||
QUrlQuery url_query;
|
QUrlQuery url_query;
|
||||||
url_query.addQueryItem(u"vendor"_s, device.vendor);
|
url_query.addQueryItem(u"vendor"_s, mtp_device.vendor);
|
||||||
url_query.addQueryItem(u"vendor_id"_s, QString::number(device.vendor_id));
|
url_query.addQueryItem(u"vendor_id"_s, QString::number(mtp_device.vendor_id));
|
||||||
url_query.addQueryItem(u"product"_s, device.product);
|
url_query.addQueryItem(u"product"_s, mtp_device.product);
|
||||||
url_query.addQueryItem(u"product_id"_s, QString::number(device.product_id));
|
url_query.addQueryItem(u"product_id"_s, QString::number(mtp_device.product_id));
|
||||||
url_query.addQueryItem(u"quirks"_s, QString::number(device.quirks));
|
url_query.addQueryItem(u"quirks"_s, QString::number(mtp_device.quirks));
|
||||||
QUrl url(str);
|
QUrl url(str);
|
||||||
url.setQuery(url_query);
|
url.setQuery(url_query);
|
||||||
return QList<QUrl>() << url;
|
return QList<QUrl>() << url;
|
||||||
}
|
}
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
if (IsCDDevice(serial)) {
|
if (IsCDDevice(serial)) {
|
||||||
return QList<QUrl>() << QUrl(u"cdda:///dev/r"_s + 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<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||||
|
|
||||||
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk.get())));
|
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk.get())));
|
||||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
|
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]);
|
||||||
QUrl ret = MakeUrlFromLocalPath(path);
|
const QUrl ret = MakeUrlFromLocalPath(path);
|
||||||
|
|
||||||
return QList<QUrl>() << ret;
|
return QList<QUrl>() << ret;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList MacOsDeviceLister::DeviceUniqueIDs() {
|
QStringList MacOsDeviceLister::DeviceUniqueIDs() {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
return current_devices_.keys() + mtp_devices_.keys();
|
return current_devices_.keys() + mtp_devices_.keys();
|
||||||
|
#else
|
||||||
|
return current_devices_.keys();
|
||||||
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
|
QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
if (IsMTPSerial(serial)) {
|
if (IsMTPSerial(serial)) {
|
||||||
return QVariantList();
|
return QVariantList();
|
||||||
}
|
}
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
if (IsCDDevice(serial)) {
|
if (IsCDDevice(serial)) {
|
||||||
return QVariantList() << u"media-optical"_s;
|
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<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||||
|
|
||||||
ScopedIOObject device(DADiskCopyIOMedia(disk.get()));
|
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<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk)));
|
||||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
|
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;
|
QVariantList ret;
|
||||||
ret << GuessIconForPath(path);
|
ret << GuessIconForPath(path);
|
||||||
@@ -784,31 +828,45 @@ QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
|
|||||||
if (!icon.isEmpty()) {
|
if (!icon.isEmpty()) {
|
||||||
ret << icon;
|
ret << icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MacOsDeviceLister::DeviceManufacturer(const QString &serial) {
|
QString MacOsDeviceLister::DeviceManufacturer(const QString &serial) {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
if (IsMTPSerial(serial)) {
|
if (IsMTPSerial(serial)) {
|
||||||
return mtp_devices_[serial].vendor;
|
return mtp_devices_[serial].vendor;
|
||||||
}
|
}
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString));
|
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MacOsDeviceLister::DeviceModel(const QString &serial) {
|
QString MacOsDeviceLister::DeviceModel(const QString &serial) {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
if (IsMTPSerial(serial)) {
|
if (IsMTPSerial(serial)) {
|
||||||
return mtp_devices_[serial].product;
|
return mtp_devices_[serial].product;
|
||||||
}
|
}
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString));
|
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
if (IsMTPSerial(serial)) {
|
if (IsMTPSerial(serial)) {
|
||||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||||
return mtp_devices_[serial].capacity;
|
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<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
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")));
|
NSNumber *capacity = reinterpret_cast<NSNumber*>(GetPropertyForDevice(device, CFSTR("Size")));
|
||||||
|
|
||||||
quint64 ret = [capacity unsignedLongLongValue];
|
const quint64 ret = [capacity unsignedLongLongValue];
|
||||||
|
|
||||||
IOObjectRelease(device);
|
IOObjectRelease(device);
|
||||||
|
|
||||||
@@ -826,10 +884,13 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
|||||||
|
|
||||||
quint64 MacOsDeviceLister::DeviceFreeSpace(const QString &serial) {
|
quint64 MacOsDeviceLister::DeviceFreeSpace(const QString &serial) {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
if (IsMTPSerial(serial)) {
|
if (IsMTPSerial(serial)) {
|
||||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||||
return mtp_devices_[serial].free_space;
|
return mtp_devices_[serial].free_space;
|
||||||
}
|
}
|
||||||
|
#endif // HAVE_MTP
|
||||||
|
|
||||||
QString bsd_name = current_devices_[serial];
|
QString bsd_name = current_devices_[serial];
|
||||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
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) {
|
void MacOsDeviceLister::UnmountDevice(const QString &serial) {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
if (IsMTPSerial(serial)) return;
|
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()));
|
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, loop_session_, bsd_name.toLatin1().constData()));
|
||||||
|
|
||||||
DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this);
|
DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this);
|
||||||
@@ -879,13 +942,16 @@ void MacOsDeviceLister::DiskUnmountCallback(DADiskRef disk, DADissenterRef disse
|
|||||||
|
|
||||||
void MacOsDeviceLister::UpdateDeviceFreeSpace(const QString &serial) {
|
void MacOsDeviceLister::UpdateDeviceFreeSpace(const QString &serial) {
|
||||||
|
|
||||||
|
#ifdef HAVE_MTP
|
||||||
if (IsMTPSerial(serial)) {
|
if (IsMTPSerial(serial)) {
|
||||||
if (mtp_devices_.contains(serial)) {
|
if (mtp_devices_.contains(serial)) {
|
||||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||||
MTPDevice *d = &mtp_devices_[serial];
|
MTPDevice *mtp_device = &mtp_devices_[serial];
|
||||||
d->free_space = GetFreeSpace(urls[0]);
|
mtp_device->free_space = GetFreeSpace(urls[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
Q_EMIT DeviceChanged(serial);
|
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