Compare commits
314 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f9de8e1d9 | |||
| 3d10414a88 | |||
| c673fd2a76 | |||
| f92419f20b | |||
| 32eee8f868 | |||
| 2cd7d6026e | |||
| 4a1c165295 | |||
| 0ac4c93a4e | |||
| 010e18ba91 | |||
| ef1ac290cd | |||
| 484ce3f737 | |||
| 49cd7a6210 | |||
| b65f33f6bd | |||
| 09c49423bf | |||
| ea18b97348 | |||
| 58dd0877e7 | |||
| e9425ba17b | |||
| 32d663e58f | |||
| a69024c0be | |||
| 81d5f57d13 | |||
| 40fadd640f | |||
|
|
1994c367c9 | ||
|
|
4915db55ba | ||
|
|
ce06115557 | ||
|
|
89d1ac8f20 | ||
|
|
891b635c64 | ||
|
|
f37b1099f3 | ||
|
|
626dd48730 | ||
|
|
6f7b8ab162 | ||
|
|
3416ede211 | ||
|
|
f8bb69ec65 | ||
|
|
64540ef6f9 | ||
|
|
cd013db33b | ||
|
|
4f554f5d5f | ||
|
|
326fe84e8a | ||
|
|
1bded170a2 | ||
|
|
a71e5b170b | ||
|
|
ea629aedd1 | ||
|
|
610b458196 | ||
|
|
ad285a91f2 | ||
|
|
6400f903e8 | ||
|
|
83d5f3d8f2 | ||
|
|
582b8e8076 | ||
|
|
030908f6ac | ||
|
|
34ae443548 | ||
|
|
1c9e99e776 | ||
|
|
4e6459b977 | ||
|
|
d2b5359fa9 | ||
|
|
1d82977441 | ||
|
|
17519076f5 | ||
|
|
e8d9e1172f | ||
|
|
aac8d4e68b | ||
|
|
0e28e800b3 | ||
|
|
cf84bc29ab | ||
|
|
afc3effc9d | ||
|
|
370bebff5f | ||
|
|
db410cc257 | ||
|
|
20a9946e51 | ||
|
|
b6c8ff19af | ||
|
|
80d058af10 | ||
|
|
da2f28811a | ||
|
|
0bfa736081 | ||
|
|
1392bcbbe1 | ||
|
|
11705889f1 | ||
|
|
604dd2dbde | ||
|
|
25065ba98f | ||
|
|
7b16ec62bb | ||
|
|
d8f31592b9 | ||
|
|
80bb0f476d | ||
|
|
b7222ac85c | ||
|
|
241bca0828 | ||
|
|
90d86b10a3 | ||
|
|
4130c6670f | ||
|
|
8d262959c1 | ||
|
|
b9b70399d8 | ||
|
|
527ccd212a | ||
|
|
4a5afbeb1e | ||
|
|
63c14e014b | ||
|
|
801658c6b9 | ||
|
|
16fe665295 | ||
|
|
2bb0dbada2 | ||
|
|
2cd9498469 | ||
|
|
d1ee27fff9 | ||
|
|
91adf5ba32 | ||
|
|
d68f464269 | ||
|
|
c684a95f89 | ||
|
|
1d03bb2178 | ||
|
|
39f9128ecf | ||
|
|
ca2e802239 | ||
|
|
9a513a9a56 | ||
|
|
1c2e87b741 | ||
|
|
fe4d9979ce | ||
|
|
d8ae790ebf | ||
|
|
ac31d79294 | ||
|
|
4ffebd77b1 | ||
|
|
6682efae2f | ||
|
|
480161c6b7 | ||
|
|
a8ba420d72 | ||
|
|
fc0ec91652 | ||
|
|
0701b97324 | ||
|
|
3867932e1e | ||
|
|
e2907f6051 | ||
|
|
0827ec7f16 | ||
|
|
24d2adf363 | ||
|
|
592729d00b | ||
|
|
c4a564bb56 | ||
|
|
812a02a3a1 | ||
|
|
944936914b | ||
|
|
e7c901d4f3 | ||
|
|
8e996119af | ||
|
|
4348a654ca | ||
|
|
f0be1c782a | ||
|
|
e9898d08bc | ||
|
|
1ad13cd3b0 | ||
|
|
5c640e0e36 | ||
|
|
059def8d0c | ||
|
|
cf15a1f423 | ||
|
|
5d35b0eedd | ||
|
|
5fcb71d08f | ||
|
|
15c2237d4a | ||
|
|
93af866185 | ||
|
|
109ff90401 | ||
|
|
d4b06289c3 | ||
|
|
4351c555a0 | ||
|
|
ce4db40983 | ||
|
|
d37fb7410c | ||
|
|
f1cdd71494 | ||
|
|
000ba997fb | ||
|
|
579efffd14 | ||
|
|
3a098c8a0c | ||
|
|
5bce0ae87f | ||
|
|
afe6967c46 | ||
|
|
e91bab6d42 | ||
|
|
5a64247761 | ||
|
|
9ed89afdb2 | ||
|
|
0ac338026c | ||
|
|
4e5f84a7b7 | ||
|
|
320a3c6815 | ||
|
|
72dd1d783a | ||
|
|
d2205cfe81 | ||
|
|
5830f247f6 | ||
|
|
8b4c57d933 | ||
|
|
67cec09176 | ||
|
|
2df658e1f3 | ||
|
|
f3bc9b151c | ||
|
|
b06b59d0c5 | ||
|
|
99d75ade06 | ||
|
|
3f63246068 | ||
|
|
b205a5f964 | ||
|
|
aeaef12dd4 | ||
|
|
02d76f17f7 | ||
|
|
e4e12c6fa6 | ||
|
|
270ae6085b | ||
|
|
7065a405a5 | ||
|
|
d8c72c3dd9 | ||
|
|
b65502e167 | ||
|
|
132f8df853 | ||
|
|
12e3cffe63 | ||
|
|
56a637682d | ||
|
|
d9b105f89e | ||
|
|
bd6b45e43f | ||
|
|
539172fb70 | ||
|
|
ebd92b3a7f | ||
|
|
b00ae5b210 | ||
|
|
c8e3cf981b | ||
|
|
038f69779f | ||
|
|
a4de7559ac | ||
|
|
0537b072fe | ||
|
|
2657c9f96a | ||
|
|
7e178b1f1a | ||
|
|
dd8513d02c | ||
|
|
5f0175094b | ||
|
|
b4c5b9e1e1 | ||
|
|
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 |
@@ -130,7 +130,10 @@ InsertBraces: false
|
||||
InsertTrailingCommas: None
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
KeepEmptyLines:
|
||||
AtEndOfFile: true
|
||||
AtStartOfBlock: true
|
||||
AtStartOfFile: true
|
||||
LambdaBodyIndentation: Signature
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
|
||||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,4 +0,0 @@
|
||||
github: jonaski
|
||||
patreon: jonaskvinge
|
||||
ko_fi: jonaskvinge
|
||||
custom: https://paypal.me/jonaskvinge
|
||||
133
.github/workflows/build.yml
vendored
133
.github/workflows/build.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
opensuse_version: [ 'tumbleweed', 'leap:15.6' ]
|
||||
opensuse_version: [ 'tumbleweed', 'leap:15.6', 'leap:16.0' ]
|
||||
container:
|
||||
image: opensuse/${{matrix.opensuse_version}}
|
||||
steps:
|
||||
@@ -27,11 +27,11 @@ jobs:
|
||||
- name: Upgrade packages (Leap)
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
run: zypper -n --gpg-auto-import-keys up
|
||||
- name: Install gcc (Tumbleweed)
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
- name: Install gcc
|
||||
if: matrix.opensuse_version != 'leap:15.6'
|
||||
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
|
||||
- name: Install gcc (Leap)
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
- name: Install gcc (leap:15.6)
|
||||
if: matrix.opensuse_version == 'leap:15.6'
|
||||
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
|
||||
- name: Install packages
|
||||
run: >
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
- name: Install kdsingleapplication-qt6-devel
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
if: matrix.opensuse_version != 'leap:15.6'
|
||||
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.opensuse_version == 'leap:15.6'
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -115,14 +115,14 @@ jobs:
|
||||
- name: Copy source tarball
|
||||
working-directory: build
|
||||
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
|
||||
- name: Build RPM (Tumbleweed)
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
- name: Build RPM
|
||||
if: matrix.opensuse_version != 'leap:15.6'
|
||||
env:
|
||||
RPM_BUILD_NCPUS: 4
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
- name: Build RPM (Leap)
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
- name: Build RPM (leap:15.6)
|
||||
if: matrix.opensuse_version == 'leap:15.6'
|
||||
env:
|
||||
RPM_BUILD_NCPUS: 4
|
||||
CC: gcc-14
|
||||
@@ -134,14 +134,14 @@ jobs:
|
||||
run: echo "subdir=$(echo ${{matrix.opensuse_version}} | sed 's/leap:/lp/g' | sed 's/\.//g')" > $GITHUB_OUTPUT
|
||||
- name: Upload source
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: source
|
||||
path: |
|
||||
/usr/src/packages/SOURCES/*.xz
|
||||
- name: Upload rpm
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: opensuse-${{steps.set-subdir.outputs.subdir}}
|
||||
path: |
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
fedora_version: [ '39', '40', '41', '42' ]
|
||||
fedora_version: [ '42', '43', '44' ]
|
||||
container:
|
||||
image: fedora:${{matrix.fedora_version}}
|
||||
steps:
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: fedora-${{matrix.fedora_version}}
|
||||
path: |
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
- name: Remove files
|
||||
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -333,7 +333,7 @@ jobs:
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
- name: Upload artifacts
|
||||
if: matrix.openmandriva_version != 'cooker'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: openmandriva-${{matrix.openmandriva_version}}
|
||||
path: |
|
||||
@@ -409,7 +409,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -434,7 +434,7 @@ jobs:
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: mageia-${{matrix.mageia_version}}
|
||||
path: |
|
||||
@@ -449,7 +449,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
debian_version: [ 'bookworm', 'trixie' ]
|
||||
debian_version: [ 'bookworm', 'trixie', 'forky' ]
|
||||
container:
|
||||
image: debian:${{matrix.debian_version}}
|
||||
steps:
|
||||
@@ -499,7 +499,11 @@ jobs:
|
||||
qt6-tools-dev-tools
|
||||
qt6-l10n-tools
|
||||
rapidjson-dev
|
||||
- name: Install KDSingleApplication
|
||||
if: matrix.debian_version != 'bookworm'
|
||||
run: apt install -y libkdsingleapplication-qt6-dev
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.debian_version == 'bookworm'
|
||||
run: |
|
||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||
cd KDSingleApplication
|
||||
@@ -507,7 +511,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -524,7 +528,7 @@ jobs:
|
||||
- name: Copy deb
|
||||
run: cp ../*.deb .
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: debian-${{matrix.debian_version}}
|
||||
path: |
|
||||
@@ -538,7 +542,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
|
||||
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -591,7 +595,11 @@ jobs:
|
||||
qt6-tools-dev-tools
|
||||
qt6-l10n-tools
|
||||
rapidjson-dev
|
||||
- name: Install KDSingleApplication
|
||||
if: matrix.ubuntu_version != 'noble'
|
||||
run: apt install -y libkdsingleapplication-qt6-dev
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.ubuntu_version == 'noble'
|
||||
run: |
|
||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||
cd KDSingleApplication
|
||||
@@ -599,7 +607,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -616,7 +624,7 @@ jobs:
|
||||
- name: Copy deb
|
||||
run: cp ../*.deb ../*.ddeb .
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ubuntu-${{matrix.ubuntu_version}}
|
||||
path: |
|
||||
@@ -631,7 +639,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
|
||||
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -691,7 +699,7 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y keyboxd
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -727,16 +735,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Free disk space
|
||||
run: |
|
||||
df -h
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
|
||||
sudo apt-get clean
|
||||
df -h
|
||||
- name: Build FreeBSD
|
||||
id: build-freebsd
|
||||
uses: vmactions/freebsd-vm@v1.2.0
|
||||
uses: vmactions/freebsd-vm@v1.3.7
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
mem: 8192
|
||||
prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio sparsehash rapidjson
|
||||
run: |
|
||||
set -e
|
||||
@@ -752,13 +766,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Build OpenBSD
|
||||
id: build-openbsd
|
||||
uses: vmactions/openbsd-vm@v1.1.8
|
||||
uses: vmactions/openbsd-vm@v1.3.1
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
@@ -779,7 +793,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ 'macos-13', 'macos-15' ]
|
||||
runner: [ 'macos-15-intel', 'macos-15' ]
|
||||
buildtype: [ 'release' ]
|
||||
|
||||
runs-on: ${{ matrix.runner }}
|
||||
@@ -818,14 +832,14 @@ jobs:
|
||||
rm -f uninstall.sh
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Import certificate file
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false
|
||||
uses: apple-actions/import-codesign-certs@v5
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE }}
|
||||
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
|
||||
@@ -882,9 +896,9 @@ jobs:
|
||||
run: make deploy
|
||||
|
||||
- name: Manually Codesign
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-13'
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
|
||||
working-directory: build
|
||||
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
||||
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib,libbrotlidec.1.dylib,libsoup-3.0.0.dylib,libnghttp2.14.dylib,libpsl.5.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
||||
|
||||
- name: Manually Codesign
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
|
||||
@@ -969,7 +983,7 @@ jobs:
|
||||
run: echo "cmake_buildtype=$(echo ${{env.buildtype}} | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}')" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1072,7 +1086,7 @@ jobs:
|
||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1295,6 +1309,20 @@ jobs:
|
||||
shell: bash
|
||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
||||
|
||||
- name: Show SDK versions
|
||||
shell: bash
|
||||
run: ls -la "c:/Program Files (x86)/Windows Kits/10/include"
|
||||
|
||||
- name: Set SDK version
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
run: echo "sdk_version=10.0.22621.0" >> $GITHUB_ENV
|
||||
|
||||
- name: Set SDK version
|
||||
if: matrix.arch == 'arm64'
|
||||
shell: bash
|
||||
run: echo "sdk_version=10.0.26100.0" >> $GITHUB_ENV
|
||||
|
||||
- name: Install rsync
|
||||
shell: cmd
|
||||
run: choco install --no-progress rsync
|
||||
@@ -1323,7 +1351,9 @@ jobs:
|
||||
|
||||
- name: Copy bin files
|
||||
shell: bash
|
||||
run: cp /c/strawberry/c/bin/{patch.exe,strip.exe,strings.exe,objdump.exe} ${{env.prefix_path_unix}}/bin
|
||||
run: |
|
||||
cp /c/mingw64/bin/{strip.exe,strings.exe,objdump.exe} ${{env.prefix_path_unix}}/bin
|
||||
cp /c/strawberry/c/bin/patch.exe ${{env.prefix_path_unix}}/bin
|
||||
|
||||
- name: Delete conflicting files
|
||||
shell: bash
|
||||
@@ -1377,11 +1407,11 @@ jobs:
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: ${{matrix.arch}}
|
||||
sdk: 10.0.20348.0
|
||||
sdk: ${{env.sdk_version}}
|
||||
vsversion: 2022
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1493,14 +1523,11 @@ jobs:
|
||||
run: copy ${{env.prefix_path_backslash}}\lib\gstreamer-1.0\*.dll .\gstreamer-plugins\
|
||||
|
||||
- name: Download copydlldeps.sh
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: curl -f -O -L https://raw.githubusercontent.com/strawberrymusicplayer/strawberry-mxe/master/tools/copydlldeps.sh
|
||||
|
||||
- name: Copy dependencies
|
||||
# copydlldeps.sh doesn't work with arm64 binaries.
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: >
|
||||
@@ -1517,12 +1544,6 @@ jobs:
|
||||
-F ./gstreamer-plugins
|
||||
-R ${{env.prefix_path_unix}}/bin
|
||||
|
||||
- name: Copy dependencies
|
||||
if: matrix.arch == 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: cp -v ${{env.prefix_path_unix}}/bin/{avcodec*.dll,avfilter*.dll,avformat*.dll,avutil*.dll,brotlicommon.dll,brotlidec.dll,chromaprint.dll,ebur128.dll,faad-2.dll,fdk-aac.dll,ffi-7.dll,FLAC.dll,freetype*.dll,getopt.dll,gio-2.0-0.dll,glib-2.0-0.dll,gme.dll,gmodule-2.0-0.dll,gobject-2.0-0.dll,gst-discoverer-1.0.exe,gst-launch-1.0.exe,gst-play-1.0.exe,gstadaptivedemux-1.0-0.dll,gstapp-1.0-0.dll,gstaudio-1.0-0.dll,gstbadaudio-1.0-0.dll,gstbase-1.0-0.dll,gstcodecparsers-1.0-0.dll,gstfft-1.0-0.dll,gstisoff-1.0-0.dll,gstmpegts-1.0-0.dll,gstnet-1.0-0.dll,gstpbutils-1.0-0.dll,gstreamer-1.0-0.dll,gstriff-1.0-0.dll,gstrtp-1.0-0.dll,gstrtsp-1.0-0.dll,gstsdp-1.0-0.dll,gsttag-1.0-0.dll,gsturidownloader-1.0-0.dll,gstvideo-1.0-0.dll,gstwinrt-1.0-0.dll,harfbuzz.dll,icudt*.dll,icuin*.dll,icuuc*.dll,intl-8.dll,jpeg62.dll,kdsingleapplication*.dll,libbs2b.dll,libcrypto-3-*.dll,fftw3.dll,libiconv*.dll,liblzma.dll,libmp3lame.dll,libopenmpt.dll,libpng16*.dll,libspeex*.dll,libssl-3-*.dll,libxml2*.dll,mpcdec.dll,mpg123.dll,nghttp2.dll,ogg.dll,opus.dll,orc-0.4-0.dll,pcre2-16*.dll,pcre2-8*.dll,postproc*.dll,psl-5.dll,Qt6Concurrent*.dll,Qt6Core*.dll,Qt6Gui*.dll,Qt6Network*.dll,Qt6Sql*.dll,Qt6Widgets*.dll,qtsparkle-qt6.dll,soup-3.0-0.dll,sqlite3.dll,sqlite3.exe,swresample*.dll,swscale*.dll,tag.dll,vorbis.dll,vorbisfile.dll,wavpackdll.dll,zlib*.dll} .
|
||||
|
||||
- name: Copy nsis files
|
||||
shell: cmd
|
||||
working-directory: build
|
||||
@@ -1623,11 +1644,11 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: sudo apt install -y git rsync
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
- name: SSH Setup
|
||||
@@ -1671,7 +1692,7 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: sudo apt install -y git jq gh
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Show release assets
|
||||
@@ -1679,7 +1700,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Add artifacts to release
|
||||
|
||||
33
.gitignore
vendored
33
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
/build
|
||||
/bin
|
||||
/CMakeLists.txt.user
|
||||
/.qtcreator
|
||||
/.kdev4
|
||||
/strawberry.kdev4
|
||||
/.vscode
|
||||
@@ -12,3 +12,34 @@
|
||||
/CMakeSettings.json
|
||||
/dist/scripts/maketarball.sh
|
||||
/debian/changelog
|
||||
_codeql_detected_source_root
|
||||
|
||||
# Build output (keep build tooling scripts in /build_tools/ tracked)
|
||||
/cmake-build*/
|
||||
/build*/
|
||||
!/build_tools/
|
||||
!/build_tools/**
|
||||
|
||||
# macOS noise
|
||||
.DS_Store
|
||||
/bin
|
||||
/CMakeLists.txt.user
|
||||
/.qtcreator
|
||||
/.kdev4
|
||||
/strawberry.kdev4
|
||||
/.vscode
|
||||
/.code-workspace
|
||||
/.sublime-workspace
|
||||
/.idea
|
||||
/.vs
|
||||
/out
|
||||
/CMakeSettings.json
|
||||
/dist/scripts/maketarball.sh
|
||||
/debian/changelog
|
||||
_codeql_detected_source_root
|
||||
|
||||
# Build output (keep build tooling scripts in /build_tools/ tracked)
|
||||
/cmake-build*/
|
||||
/build*/
|
||||
!/build_tools/
|
||||
!/build_tools/**
|
||||
|
||||
6
3rdparty/discord-rpc/CMakeLists.txt
vendored
6
3rdparty/discord-rpc/CMakeLists.txt
vendored
@@ -37,5 +37,9 @@ if(WIN32)
|
||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||
endif()
|
||||
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
if(TARGET RapidJSON::RapidJSON)
|
||||
target_link_libraries(discord-rpc PRIVATE RapidJSON::RapidJSON)
|
||||
elseif(RapidJSON_INCLUDE_DIRS)
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
endif()
|
||||
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
2
3rdparty/discord-rpc/discord_connection.h
vendored
2
3rdparty/discord-rpc/discord_connection.h
vendored
@@ -35,7 +35,7 @@ int GetProcessId();
|
||||
|
||||
struct BaseConnection {
|
||||
static BaseConnection *Create();
|
||||
static void Destroy(BaseConnection *&);
|
||||
static void Destroy(BaseConnection*&);
|
||||
bool isOpen = false;
|
||||
bool Open();
|
||||
bool Close();
|
||||
|
||||
@@ -39,11 +39,11 @@ int GetProcessId() {
|
||||
}
|
||||
|
||||
struct BaseConnectionUnix : public BaseConnection {
|
||||
int sock { -1 };
|
||||
int sock{ -1 };
|
||||
};
|
||||
|
||||
static BaseConnectionUnix Connection;
|
||||
static sockaddr_un PipeAddr {};
|
||||
static sockaddr_un PipeAddr{};
|
||||
#ifdef MSG_NOSIGNAL
|
||||
static int MsgFlags = MSG_NOSIGNAL;
|
||||
#else
|
||||
@@ -105,7 +105,7 @@ bool BaseConnection::Open() {
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
12
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
12
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
@@ -38,7 +38,7 @@ int GetProcessId() {
|
||||
}
|
||||
|
||||
struct BaseConnectionWin : public BaseConnection {
|
||||
HANDLE pipe { INVALID_HANDLE_VALUE };
|
||||
HANDLE pipe{ INVALID_HANDLE_VALUE };
|
||||
};
|
||||
|
||||
static BaseConnectionWin Connection;
|
||||
@@ -57,10 +57,10 @@ void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
|
||||
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;
|
||||
pipeName[pipeDigit] = L'0';
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
for (;;) {
|
||||
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (self->pipe != INVALID_HANDLE_VALUE) {
|
||||
@@ -88,7 +88,7 @@ bool BaseConnection::Open() {
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
::CloseHandle(self->pipe);
|
||||
self->pipe = INVALID_HANDLE_VALUE;
|
||||
self->isOpen = false;
|
||||
@@ -102,7 +102,7 @@ bool BaseConnection::Write(const void *data, size_t length) {
|
||||
if (length == 0) {
|
||||
return true;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
@@ -127,7 +127,7 @@ bool BaseConnection::Read(void *data, size_t length) {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
|
||||
6
3rdparty/discord-rpc/discord_msg_queue.h
vendored
6
3rdparty/discord-rpc/discord_msg_queue.h
vendored
@@ -34,9 +34,9 @@ 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 };
|
||||
std::atomic_uint nextAdd_{ 0 };
|
||||
std::atomic_uint nextSend_{ 0 };
|
||||
std::atomic_uint pendingSends_{ 0 };
|
||||
|
||||
public:
|
||||
MsgQueue() {}
|
||||
|
||||
@@ -163,4 +163,3 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
|
||||
Discord_RegisterW(appId, wcommand);
|
||||
|
||||
}
|
||||
|
||||
|
||||
68
3rdparty/discord-rpc/discord_rpc.cpp
vendored
68
3rdparty/discord-rpc/discord_rpc.cpp
vendored
@@ -40,9 +40,9 @@ static void Discord_UpdateConnection();
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MaxMessageSize { 16 * 1024 };
|
||||
constexpr size_t MessageQueueSize { 8 };
|
||||
constexpr size_t JoinQueueSize { 8 };
|
||||
constexpr size_t MaxMessageSize{ 16 * 1024 };
|
||||
constexpr size_t MessageQueueSize{ 8 };
|
||||
constexpr size_t JoinQueueSize{ 8 };
|
||||
|
||||
struct QueuedMessage {
|
||||
size_t length;
|
||||
@@ -70,24 +70,24 @@ struct User {
|
||||
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
|
||||
};
|
||||
|
||||
static RpcConnection *Connection { nullptr };
|
||||
static DiscordEventHandlers QueuedHandlers {};
|
||||
static DiscordEventHandlers Handlers {};
|
||||
static std::atomic_bool WasJustConnected { false };
|
||||
static std::atomic_bool WasJustDisconnected { false };
|
||||
static std::atomic_bool GotErrorMessage { false };
|
||||
static std::atomic_bool WasJoinGame { false };
|
||||
static std::atomic_bool WasSpectateGame { false };
|
||||
static std::atomic_bool UpdatePresence { false };
|
||||
static RpcConnection *Connection{ nullptr };
|
||||
static DiscordEventHandlers QueuedHandlers{};
|
||||
static DiscordEventHandlers Handlers{};
|
||||
static std::atomic_bool WasJustConnected{ false };
|
||||
static std::atomic_bool WasJustDisconnected{ false };
|
||||
static std::atomic_bool GotErrorMessage{ false };
|
||||
static std::atomic_bool WasJoinGame{ false };
|
||||
static std::atomic_bool WasSpectateGame{ false };
|
||||
static std::atomic_bool UpdatePresence{ false };
|
||||
static char JoinGameSecret[256];
|
||||
static char SpectateGameSecret[256];
|
||||
static int LastErrorCode { 0 };
|
||||
static int LastErrorCode{ 0 };
|
||||
static char LastErrorMessage[256];
|
||||
static int LastDisconnectErrorCode { 0 };
|
||||
static int LastDisconnectErrorCode{ 0 };
|
||||
static char LastDisconnectErrorMessage[256];
|
||||
static std::mutex PresenceMutex;
|
||||
static std::mutex HandlerMutex;
|
||||
static QueuedMessage QueuedPresence {};
|
||||
static QueuedMessage QueuedPresence{};
|
||||
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
||||
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
||||
static User connectedUser;
|
||||
@@ -95,12 +95,12 @@ static User connectedUser;
|
||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
|
||||
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
||||
static auto NextConnect = std::chrono::system_clock::now();
|
||||
static int Pid { 0 };
|
||||
static int Nonce { 1 };
|
||||
static int Pid{ 0 };
|
||||
static int Nonce{ 1 };
|
||||
|
||||
class IoThreadHolder {
|
||||
private:
|
||||
std::atomic_bool keepRunning { true };
|
||||
std::atomic_bool keepRunning{ true };
|
||||
std::mutex waitForIOMutex;
|
||||
std::condition_variable waitForIOActivity;
|
||||
std::thread ioThread;
|
||||
@@ -109,14 +109,14 @@ class IoThreadHolder {
|
||||
void Start() {
|
||||
keepRunning.store(true);
|
||||
ioThread = std::thread([&]() {
|
||||
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
|
||||
Discord_UpdateConnection();
|
||||
while (keepRunning.load()) {
|
||||
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
||||
waitForIOActivity.wait_for(lock, maxWait);
|
||||
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
|
||||
Discord_UpdateConnection();
|
||||
}
|
||||
});
|
||||
while (keepRunning.load()) {
|
||||
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
||||
waitForIOActivity.wait_for(lock, maxWait);
|
||||
Discord_UpdateConnection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Notify() { waitForIOActivity.notify_all(); }
|
||||
@@ -132,7 +132,7 @@ class IoThreadHolder {
|
||||
~IoThreadHolder() { Stop(); }
|
||||
};
|
||||
|
||||
static IoThreadHolder *IoThread { nullptr };
|
||||
static IoThreadHolder *IoThread{ nullptr };
|
||||
|
||||
static void UpdateReconnectTime() {
|
||||
|
||||
@@ -429,7 +429,7 @@ extern "C" void Discord_RunCallbacks() {
|
||||
if (WasJustConnected.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.ready) {
|
||||
DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||
DiscordUser du{ connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||
Handlers.ready(&du);
|
||||
}
|
||||
}
|
||||
@@ -465,7 +465,7 @@ extern "C" void Discord_RunCallbacks() {
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinRequest) {
|
||||
DiscordUser du { req->userId, req->username, req->discriminator, req->avatar };
|
||||
DiscordUser du{ req->userId, req->username, req->discriminator, req->avatar };
|
||||
Handlers.joinRequest(&du);
|
||||
}
|
||||
}
|
||||
@@ -486,12 +486,12 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
||||
|
||||
if (newHandlers) {
|
||||
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||
RegisterForEvent(event); \
|
||||
} \
|
||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
||||
DeregisterForEvent(event); \
|
||||
}
|
||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||
RegisterForEvent(event); \
|
||||
} \
|
||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
||||
DeregisterForEvent(event); \
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
|
||||
|
||||
1
3rdparty/discord-rpc/discord_rpc.h
vendored
1
3rdparty/discord-rpc/discord_rpc.h
vendored
@@ -32,6 +32,7 @@ extern "C" {
|
||||
|
||||
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 */
|
||||
|
||||
12
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
12
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
@@ -63,17 +63,17 @@ struct RpcConnection {
|
||||
Connected,
|
||||
};
|
||||
|
||||
BaseConnection *connection { nullptr };
|
||||
State state { State::Disconnected };
|
||||
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] {};
|
||||
char appId[64]{};
|
||||
int lastErrorCode{ 0 };
|
||||
char lastErrorMessage[256]{};
|
||||
RpcConnection::MessageFrame sendFrame;
|
||||
|
||||
static RpcConnection *Create(const char *applicationId);
|
||||
static void Destroy(RpcConnection *&);
|
||||
static void Destroy(RpcConnection*&);
|
||||
|
||||
inline bool IsOpen() const { return state == State::Connected; }
|
||||
|
||||
|
||||
@@ -128,6 +128,9 @@ size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce
|
||||
if (presence->type >= 0 && presence->type <= 5) {
|
||||
WriteKey(writer, "type");
|
||||
writer.Int(presence->type);
|
||||
|
||||
WriteKey(writer, "status_display_type");
|
||||
writer.Int(presence->status_display_type);
|
||||
}
|
||||
|
||||
WriteOptionalString(writer, "name", presence->name);
|
||||
|
||||
63
Brewfile
Normal file
63
Brewfile
Normal file
@@ -0,0 +1,63 @@
|
||||
# Strawberry Music Player (macOS) - Homebrew Bundle
|
||||
#
|
||||
# Usage:
|
||||
# brew bundle --file Brewfile
|
||||
#
|
||||
# Notes:
|
||||
# - This is intended for macOS (Apple Silicon or Intel).
|
||||
# - Some Strawberry features are optional and will auto-disable if deps are missing.
|
||||
|
||||
# Build tooling
|
||||
brew "cmake"
|
||||
brew "pkg-config"
|
||||
brew "ninja"
|
||||
|
||||
# Optional (developer): unit tests
|
||||
brew "googletest"
|
||||
|
||||
# Core runtime/build dependencies (required by CMakeLists.txt)
|
||||
brew "qt" # Qt 6 (Core/Gui/Widgets/Network/Sql/Concurrent)
|
||||
brew "vulkan-headers" # helps Qt6Gui's WrapVulkanHeaders dependency on some setups
|
||||
brew "boost"
|
||||
brew "icu4c"
|
||||
brew "glib" # provides glib-2.0 + gobject-2.0 (via pkg-config)
|
||||
brew "glib-networking" # TLS + GIO modules (helps macOS bundling via dist/macos/macgstcopy.sh)
|
||||
brew "sqlite"
|
||||
brew "taglib"
|
||||
brew "gstreamer"
|
||||
|
||||
# Strawberry requires KDAB's KDSingleApplication (CMake package name: KDSingleApplication-qt6).
|
||||
# Homebrew core doesn't consistently provide it, so this repo includes a local formula.
|
||||
# Homebrew requires formulae to be installed from a tap; `brew bundle` will tap *this repo*
|
||||
# using the current working directory (run `brew bundle` from the repo root).
|
||||
# If you previously tapped `strawberry/local` before `Formula/` existed, refresh it with:
|
||||
# brew untap strawberry/local && brew tap strawberry/local "file://$PWD"
|
||||
tap "strawberry/local", "file://#{Dir.pwd}"
|
||||
brew "strawberry/local/kdsingleapplication-qt6"
|
||||
brew "strawberry/local/qtsparkle-qt6" # optional: QtSparkle integration
|
||||
brew "strawberry/local/sparkle-framework" # optional: Sparkle integration (framework)
|
||||
brew "strawberry/local/macdeploycheck" # optional: enables CMake target 'deploycheck' (sanity checks deployed .app)
|
||||
|
||||
# Recommended GStreamer plugin sets for broad codec support (matches README guidance)
|
||||
brew "gst-plugins-base"
|
||||
brew "gst-plugins-good"
|
||||
brew "gst-plugins-bad"
|
||||
brew "gst-plugins-ugly"
|
||||
brew "gst-libav"
|
||||
|
||||
# Optional features (silences CMake warnings / enables extra functionality)
|
||||
brew "rapidjson" # enables Discord Rich Presence (DISCORD_RPC)
|
||||
brew "google-sparsehash" # enables stream tagreader (STREAMTAGREADER / libsparsehash)
|
||||
brew "chromaprint" # enables MusicBrainz + song fingerprinting
|
||||
brew "fftw" # enables Moodbar (fftw3)
|
||||
brew "libebur128" # enables EBU R 128 loudness normalization
|
||||
brew "libcdio" # enables Audio CD support
|
||||
brew "libmtp" # enables MTP device support
|
||||
brew "strawberry/local/libgpod" # enables iPod classic support (Homebrew core doesn't provide libgpod)
|
||||
|
||||
# Helpful for Strawberry's macOS "deploy" target (GStreamer dynamically loads libsoup)
|
||||
brew "libsoup"
|
||||
|
||||
# Optional: enable building the CMake "dmg" target (cmake/Dmg.cmake)
|
||||
brew "create-dmg"
|
||||
|
||||
@@ -32,6 +32,18 @@ if(LINUX)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
# Find Sparkle early so cmake/Dmg.cmake (deploy target) can bundle it into the app.
|
||||
# Sparkle is optional; if not found, update functionality is disabled.
|
||||
find_library(SPARKLE Sparkle
|
||||
PATHS
|
||||
/Library/Frameworks
|
||||
/System/Library/Frameworks
|
||||
/opt/homebrew/Frameworks
|
||||
/opt/homebrew/opt/sparkle-framework/Frameworks
|
||||
/usr/local/Frameworks
|
||||
/usr/local/opt/sparkle-framework/Frameworks
|
||||
PATH_SUFFIXES Frameworks
|
||||
)
|
||||
include(cmake/Dmg.cmake)
|
||||
endif()
|
||||
|
||||
@@ -84,8 +96,6 @@ if(MSVC)
|
||||
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
|
||||
else()
|
||||
list(APPEND COMPILE_OPTIONS
|
||||
$<$<COMPILE_LANGUAGE:C>:-std=c11>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wpedantic
|
||||
@@ -253,17 +263,23 @@ endif()
|
||||
|
||||
find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
|
||||
|
||||
if(APPLE)
|
||||
find_library(SPARKLE Sparkle)
|
||||
#find_package(SPMediaKeyTap REQUIRED)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
find_package(getopt-win REQUIRED)
|
||||
find_package(getopt NAMES getopt getopt-win unofficial-getopt-win32 REQUIRED)
|
||||
if(TARGET getopt::getopt)
|
||||
set(GETOPT_LIBRARIES getopt::getopt)
|
||||
elseif(TARGET getopt-win::getopt)
|
||||
set(GETOPT_LIBRARIES getopt-win::getopt)
|
||||
elseif(TARGET getopt::getopt_shared)
|
||||
set(GETOPT_LIBRARIES getopt::getopt_shared)
|
||||
elseif(TARGET unofficial::getopt-win32::getopt)
|
||||
set(GETOPT_LIBRARIES unofficial::getopt-win32::getopt)
|
||||
else()
|
||||
message(FATAL_ERROR "Missing getopt")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(APPLE OR WIN32)
|
||||
find_package(qtsparkle-qt${QT_VERSION_MAJOR})
|
||||
find_package(qtsparkle-qt${QT_VERSION_MAJOR} QUIET)
|
||||
if(TARGET "qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle")
|
||||
set(QTSPARKLE_FOUND ON)
|
||||
endif()
|
||||
@@ -682,6 +698,7 @@ set(SOURCES
|
||||
src/lyrics/htmllyricsprovider.cpp
|
||||
src/lyrics/ovhlyricsprovider.cpp
|
||||
src/lyrics/lololyricsprovider.cpp
|
||||
src/lyrics/geniuslyricsprovider.cpp
|
||||
src/lyrics/musixmatchlyricsprovider.cpp
|
||||
src/lyrics/chartlyricsprovider.cpp
|
||||
src/lyrics/songlyricscomlyricsprovider.cpp
|
||||
@@ -689,6 +706,7 @@ set(SOURCES
|
||||
src/lyrics/elyricsnetlyricsprovider.cpp
|
||||
src/lyrics/letraslyricsprovider.cpp
|
||||
src/lyrics/lyricfindlyricsprovider.cpp
|
||||
src/lyrics/lrcliblyricsprovider.cpp
|
||||
|
||||
src/settings/settingsdialog.cpp
|
||||
src/settings/settingspage.cpp
|
||||
@@ -782,9 +800,7 @@ set(SOURCES
|
||||
src/scrobbler/scrobblercache.cpp
|
||||
src/scrobbler/scrobblercacheitem.cpp
|
||||
src/scrobbler/scrobblemetadata.cpp
|
||||
src/scrobbler/scrobblingapi20.cpp
|
||||
src/scrobbler/lastfmscrobbler.cpp
|
||||
src/scrobbler/librefmscrobbler.cpp
|
||||
src/scrobbler/listenbrainzscrobbler.cpp
|
||||
src/scrobbler/lastfmimport.cpp
|
||||
|
||||
@@ -812,6 +828,8 @@ set(SOURCES
|
||||
|
||||
src/fileview/fileview.cpp
|
||||
src/fileview/fileviewlist.cpp
|
||||
src/fileview/fileviewtree.cpp
|
||||
src/fileview/fileviewtreemodel.cpp
|
||||
|
||||
src/device/devicemanager.cpp
|
||||
src/device/devicelister.cpp
|
||||
@@ -979,6 +997,7 @@ set(HEADERS
|
||||
src/lyrics/htmllyricsprovider.h
|
||||
src/lyrics/ovhlyricsprovider.h
|
||||
src/lyrics/lololyricsprovider.h
|
||||
src/lyrics/geniuslyricsprovider.h
|
||||
src/lyrics/musixmatchlyricsprovider.h
|
||||
src/lyrics/chartlyricsprovider.h
|
||||
src/lyrics/songlyricscomlyricsprovider.h
|
||||
@@ -986,6 +1005,7 @@ set(HEADERS
|
||||
src/lyrics/elyricsnetlyricsprovider.h
|
||||
src/lyrics/letraslyricsprovider.h
|
||||
src/lyrics/lyricfindlyricsprovider.h
|
||||
src/lyrics/lrcliblyricsprovider.h
|
||||
|
||||
src/settings/settingsdialog.h
|
||||
src/settings/settingspage.h
|
||||
@@ -1074,9 +1094,7 @@ set(HEADERS
|
||||
src/scrobbler/scrobblersettingsservice.h
|
||||
src/scrobbler/scrobblerservice.h
|
||||
src/scrobbler/scrobblercache.h
|
||||
src/scrobbler/scrobblingapi20.h
|
||||
src/scrobbler/lastfmscrobbler.h
|
||||
src/scrobbler/librefmscrobbler.h
|
||||
src/scrobbler/listenbrainzscrobbler.h
|
||||
src/scrobbler/lastfmimport.h
|
||||
|
||||
@@ -1101,6 +1119,8 @@ set(HEADERS
|
||||
|
||||
src/fileview/fileview.h
|
||||
src/fileview/fileviewlist.h
|
||||
src/fileview/fileviewtree.h
|
||||
src/fileview/fileviewtreemodel.h
|
||||
|
||||
src/device/devicemanager.h
|
||||
src/device/devicelister.h
|
||||
@@ -1203,6 +1223,10 @@ set(UI
|
||||
src/device/deviceviewcontainer.ui
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
optional_source(UNIX SOURCES src/core/unixsignalwatcher.cpp HEADERS src/core/unixsignalwatcher.h)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
optional_source(APPLE
|
||||
SOURCES
|
||||
@@ -1431,6 +1455,7 @@ optional_source(HAVE_SPOTIFY
|
||||
src/spotify/spotifybaserequest.cpp
|
||||
src/spotify/spotifyrequest.cpp
|
||||
src/spotify/spotifyfavoriterequest.cpp
|
||||
src/spotify/spotifymetadatarequest.cpp
|
||||
src/settings/spotifysettingspage.cpp
|
||||
src/covermanager/spotifycoverprovider.cpp
|
||||
HEADERS
|
||||
@@ -1438,6 +1463,7 @@ optional_source(HAVE_SPOTIFY
|
||||
src/spotify/spotifybaserequest.h
|
||||
src/spotify/spotifyrequest.h
|
||||
src/spotify/spotifyfavoriterequest.h
|
||||
src/spotify/spotifymetadatarequest.h
|
||||
src/settings/spotifysettingspage.h
|
||||
src/covermanager/spotifycoverprovider.h
|
||||
UI
|
||||
@@ -1452,6 +1478,8 @@ optional_source(HAVE_QOBUZ
|
||||
src/qobuz/qobuzrequest.cpp
|
||||
src/qobuz/qobuzstreamurlrequest.cpp
|
||||
src/qobuz/qobuzfavoriterequest.cpp
|
||||
src/qobuz/qobuzmetadatarequest.cpp
|
||||
src/qobuz/qobuzcredentialfetcher.cpp
|
||||
src/settings/qobuzsettingspage.cpp
|
||||
src/covermanager/qobuzcoverprovider.cpp
|
||||
HEADERS
|
||||
@@ -1461,6 +1489,8 @@ optional_source(HAVE_QOBUZ
|
||||
src/qobuz/qobuzrequest.h
|
||||
src/qobuz/qobuzstreamurlrequest.h
|
||||
src/qobuz/qobuzfavoriterequest.h
|
||||
src/qobuz/qobuzmetadatarequest.h
|
||||
src/qobuz/qobuzcredentialfetcher.h
|
||||
src/settings/qobuzsettingspage.h
|
||||
src/covermanager/qobuzcoverprovider.h
|
||||
UI
|
||||
@@ -1496,10 +1526,22 @@ if(HAVE_DISCORD_RPC)
|
||||
endif()
|
||||
|
||||
if(HAVE_TRANSLATIONS)
|
||||
option(TRANSLATIONS_VERBOSE "Show verbose output while generating .qm translation files" OFF)
|
||||
# On non-Windows platforms Qt doesn't need a PATH-setup wrapper for tools, but we can
|
||||
# provide a wrapper to filter non-actionable lrelease noise during normal builds.
|
||||
if(NOT CMAKE_HOST_WIN32)
|
||||
set(QT_TOOL_COMMAND_WRAPPER_PATH "${CMAKE_SOURCE_DIR}/cmake/qt_tool_wrapper.sh"
|
||||
CACHE INTERNAL "Wrapper used when invoking Qt tools from CMake" FORCE
|
||||
)
|
||||
endif()
|
||||
qt_add_lupdate(strawberry_lib TS_FILES "${CMAKE_SOURCE_DIR}/src/translations/strawberry_en_US.ts" OPTIONS -locations none -no-ui-lines -no-obsolete)
|
||||
file(GLOB_RECURSE ts_files ${CMAKE_SOURCE_DIR}/src/translations/*.ts)
|
||||
set_source_files_properties(${ts_files} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/data")
|
||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES)
|
||||
if(TRANSLATIONS_VERBOSE)
|
||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES)
|
||||
else()
|
||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES OPTIONS -silent)
|
||||
endif()
|
||||
if(NOT INSTALL_TRANSLATIONS)
|
||||
qt_add_resources(strawberry "translations" PREFIX "/i18n" BASE "${CMAKE_CURRENT_BINARY_DIR}/data" FILES "${INSTALL_TRANSLATIONS_FILES}")
|
||||
endif()
|
||||
@@ -1552,7 +1594,7 @@ target_link_libraries(strawberry_lib PUBLIC
|
||||
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
|
||||
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
|
||||
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
|
||||
$<$<BOOL:${WIN32}>:dsound dwmapi getopt-win::getopt>
|
||||
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
|
||||
$<$<BOOL:${MSVC}>:WindowsApp>
|
||||
KDAB::kdsingleapplication
|
||||
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
|
||||
|
||||
121
Changelog
121
Changelog
@@ -2,6 +2,125 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
Version 1.2.17 (2026.01.18):
|
||||
|
||||
* Avoid re-scan of restored songs unless mtime is changed (#1819)
|
||||
* Skip existing files when organizing if not overwriting (#1484)
|
||||
* Fixed cursor highlight disappearing off-screen when using down cursor (#1489)
|
||||
* Fixed CD playback only working for the first optical drive (#1852)
|
||||
* Fixed possible race-condition when switching tracks (#1863)
|
||||
* Fixed possible file descriptor exhaustion by using shared thread pool (#1687)
|
||||
* Don't automatically sort playlist with the auto sort option before it's fully loaded (#1690)
|
||||
* Fixed network features stop working after computer suspends and resumes (#1521)
|
||||
* Fixed crash on exit after Qobuz login
|
||||
* Added tag editor option to select ID3v2 version (#1861)
|
||||
* Fixed Qobuz authentication and added automatic credential fetching (#1898)
|
||||
* Fixed playback stopping after deleting a song from disk via context menu (#1783)
|
||||
* Added option to restore smart playlists to the defaults (#1848)
|
||||
* Fixed possible race condition in pipeline destructor (#1875)
|
||||
* Fixed buffering issue near track end during gapless playback (#1725)
|
||||
* Fixed duplicate collection entries for the same artist if they have different sort tags (#1899)
|
||||
* Defer playcount and rating tag writes for currently playing Ogg songs to prevent playback shutter (#1816)
|
||||
* Fixed tag editing not working for Opus sort tags (#1929)
|
||||
* Show playlist load errors (#1470)
|
||||
* Fallback to delete if moving to trash fails (#1679)
|
||||
* Prefer filenames with "front" or "cover" in the filename for album cover art for songs outside of the collection (#1745)
|
||||
* Fixed collection enter/return behavior to respect double-click settings (#1691)
|
||||
* Added tree view mode to files tab (#1922)
|
||||
* Include .webp in allowed extensions for album covers (#1941)
|
||||
* Exit gracefully on SIGTERM signal for Unix systems (#1942)
|
||||
* Optimize the collection scanning process by deferring media file validation from the initial directory scan (#1954)
|
||||
* Fixed collection scan not finding new directories in the top level collection directory when the mountpoint is restored (#1914)
|
||||
* Added genre metadata parsing for Tidal, Qobuz and Spotify (#1913)
|
||||
* Allow editing metadata for stream songs (#1913)
|
||||
* Optimized collection/playlist filtering
|
||||
* Added sort tags to collection/playlist filtering (#1966)
|
||||
|
||||
Version 1.2.16 (2025.12.16):
|
||||
|
||||
* Make Discord Rich presence use filename if song title is missing
|
||||
* Added better error message when a GStreamer plugin is missing
|
||||
* Preserve track order in album shuffle mode when restarting playback (#1623)
|
||||
* Possible fixes for context word wrap
|
||||
* Added lyrics from lrclib.net
|
||||
* Added option to turn off the use of sort tags for the collection
|
||||
* Fixed Spotify login
|
||||
* Fixed error dialog shown minimized if another Strawberry window than the mainwindow was active
|
||||
* Fixed seeking to the end of the track and back causing seeking to stop working (#1675)
|
||||
* Set current index when automatically selecting track (#1825)
|
||||
* Make icon size for shuffle and repeat buttons adjust to screen resolution (#1838)
|
||||
* Fixed song being removed from playlist when dragging to another application (#1815)
|
||||
* Don't automatically scroll on dynamic playlists (#1427)
|
||||
|
||||
Version 1.2.15 (2025.11.25):
|
||||
|
||||
* Fixed system default language not respected
|
||||
* Fixed length filter search
|
||||
* Fixed playlist parser converting Spotify URL's
|
||||
* Removed use of deprecated QStyle::State_Editing
|
||||
* Ignore connection closed errors for ListenBrainz
|
||||
* (Windows) Support building with vcpkg unofficial::getopt-win32::getopt
|
||||
|
||||
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.
|
||||
@@ -224,7 +343,7 @@ Version 1.1.0 (2024.07.14):
|
||||
* Only use playbin3 with GStreamer 1.24 and higher, not with GStreamer 1.22 or lower.
|
||||
* (macOS/Windows) Fixed dash and hls streaming, plugins were missing.
|
||||
* (Windows) Fixed incorrect colors in smart playlist wizard with Fusion in dark mode (#1399).
|
||||
* (Windows) Fixed update window blocking sponsor window on startup.
|
||||
* (Windows) Fixed update window blocking startup window on launch.
|
||||
|
||||
Enhancements:
|
||||
* Improve error messages when connecting and copying to devices.
|
||||
|
||||
51
Formula/kdsingleapplication-qt6.rb
Normal file
51
Formula/kdsingleapplication-qt6.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
class KdsingleapplicationQt6 < Formula
|
||||
desc "Helper class for single-instance Qt applications (Qt 6 build)"
|
||||
homepage "https://github.com/KDAB/KDSingleApplication"
|
||||
url "https://github.com/KDAB/KDSingleApplication/archive/refs/tags/v1.1.0.tar.gz"
|
||||
sha256 "1f19124c0aa5c6fffee3da174f7d2e091fab6dca1e123da70bb0fe615bfbe3e8"
|
||||
license "MIT"
|
||||
|
||||
depends_on "cmake" => :build
|
||||
depends_on "ninja" => :build
|
||||
depends_on "qt"
|
||||
|
||||
def install
|
||||
args = std_cmake_args + %W[
|
||||
-GNinja
|
||||
-DKDSingleApplication_QT6=ON
|
||||
-DKDSingleApplication_TESTS=OFF
|
||||
-DKDSingleApplication_EXAMPLES=OFF
|
||||
-DKDSingleApplication_DOCS=OFF
|
||||
-DKDSingleApplication_DEVELOPER_MODE=OFF
|
||||
-DKDSingleApplication_STATIC=OFF
|
||||
]
|
||||
|
||||
system "cmake", "-S", ".", "-B", "build", *args
|
||||
system "cmake", "--build", "build"
|
||||
system "cmake", "--install", "build"
|
||||
end
|
||||
|
||||
test do
|
||||
# Verify CMake package is usable via find_package(KDSingleApplication-qt6 CONFIG REQUIRED)
|
||||
(testpath/"CMakeLists.txt").write <<~CMAKE
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(kdsa_test LANGUAGES CXX)
|
||||
find_package(KDSingleApplication-qt6 CONFIG REQUIRED)
|
||||
add_executable(test_kdsa main.cpp)
|
||||
target_link_libraries(test_kdsa PRIVATE KDAB::kdsingleapplication)
|
||||
CMAKE
|
||||
|
||||
(testpath/"main.cpp").write <<~CPP
|
||||
#include <QCoreApplication>
|
||||
int main(int argc, char** argv) {
|
||||
QCoreApplication app(argc, argv);
|
||||
return 0;
|
||||
}
|
||||
CPP
|
||||
|
||||
system "cmake", "-S", ".", "-B", "build",
|
||||
"-DCMAKE_PREFIX_PATH=#{opt_prefix}"
|
||||
system "cmake", "--build", "build"
|
||||
end
|
||||
end
|
||||
|
||||
25
Formula/kdsingleapplication-qt6/README.md
Normal file
25
Formula/kdsingleapplication-qt6/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# kdsingleapplication-qt6 (local Homebrew formula)
|
||||
|
||||
This directory exists to keep any supporting files for the local Homebrew formula
|
||||
next to it (e.g. patches or notes).
|
||||
|
||||
## Install (from this Strawberry repo)
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
brew tap strawberry/local "file://$PWD"
|
||||
brew install strawberry/local/kdsingleapplication-qt6
|
||||
```
|
||||
|
||||
## Why it exists
|
||||
|
||||
Strawberry’s build requires the CMake package `KDSingleApplication-qt6`, but it is
|
||||
not consistently available via Homebrew core. Shipping a local formula makes the
|
||||
dependency easy to install for anyone building this repo on macOS.
|
||||
|
||||
## Note for local development
|
||||
|
||||
Homebrew taps are Git clones, so the formula must be committed (or pushed to a remote)
|
||||
to be visible to `brew tap`.
|
||||
|
||||
81
Formula/libgpod.rb
Normal file
81
Formula/libgpod.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
class Libgpod < Formula
|
||||
desc "Library to access the contents of classic iPods"
|
||||
homepage "https://gtkpod.org/libgpod/"
|
||||
url "https://github.com/neuschaefer/libgpod/archive/0dda196286f5e42be89f0b870abd9278213989a5.tar.gz"
|
||||
sha256 "a9809f85b2b763196ac7c94903211a927efd37a24ef39c355c21b4a1bed28e52"
|
||||
license "LGPL-2.0-only"
|
||||
|
||||
depends_on "autoconf" => :build
|
||||
depends_on "automake" => :build
|
||||
depends_on "libtool" => :build
|
||||
depends_on "pkg-config" => :build
|
||||
depends_on "gtk-doc" => :build
|
||||
depends_on "intltool" => :build
|
||||
|
||||
depends_on "glib"
|
||||
depends_on "gdk-pixbuf"
|
||||
depends_on "libplist"
|
||||
depends_on "libxml2"
|
||||
depends_on "sqlite"
|
||||
|
||||
def install
|
||||
# libgpod's configure.ac checks for pkg-config module name "libplist".
|
||||
# Homebrew provides "libplist-2.0", so we provide a tiny shim .pc file to
|
||||
# satisfy the expected name.
|
||||
(buildpath/"brew-pkgconfig").mkpath
|
||||
(buildpath/"brew-pkgconfig/libplist.pc").write <<~EOS
|
||||
prefix=#{Formula["libplist"].opt_prefix}
|
||||
exec_prefix=${prefix}
|
||||
libdir=#{Formula["libplist"].opt_lib}
|
||||
includedir=#{Formula["libplist"].opt_include}
|
||||
|
||||
Name: libplist
|
||||
Description: Apple property list library (Homebrew shim for libgpod)
|
||||
Version: #{Formula["libplist"].version}
|
||||
Libs: -L${libdir} -lplist-2.0
|
||||
Cflags: -I${includedir}
|
||||
EOS
|
||||
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", buildpath/"brew-pkgconfig"
|
||||
|
||||
# Ensure pkg-config can find Homebrew keg .pc files during configure.
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["libplist"].opt_lib/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["sqlite"].opt_lib/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["glib"].opt_lib/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["glib"].opt_share/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["gdk-pixbuf"].opt_lib/"pkgconfig"
|
||||
|
||||
# Upstream's autogen.sh is very old and may hardcode ancient automake checks
|
||||
# (e.g. looking for automake-1.7). Using autoreconf is the standard Homebrew
|
||||
# way and works with modern autotools.
|
||||
#
|
||||
# libgpod's build system expects gtk-doc's makefile snippet to exist (gtk-doc.make),
|
||||
# which is normally provided by running gtkdocize.
|
||||
system "gtkdocize", "--copy"
|
||||
|
||||
# libgpod also uses intltool's Autoconf macros (IT_PROG_INTLTOOL). If intltoolize
|
||||
# is not run, the generated ./configure may contain unexpanded macros and fail.
|
||||
system "intltoolize", "--force", "--copy", "--automake"
|
||||
system "autoreconf", "-fiv"
|
||||
|
||||
system "./configure", *std_configure_args,
|
||||
"--disable-dependency-tracking",
|
||||
"--with-hal=no",
|
||||
"--disable-udev",
|
||||
"--without-libimobiledevice",
|
||||
"--with-python=no",
|
||||
"--with-mono=no",
|
||||
"--disable-gtk-doc",
|
||||
"--disable-gtk-doc-html",
|
||||
"--disable-gtk-doc-pdf",
|
||||
"--enable-more-warnings=no"
|
||||
|
||||
system "make", "install"
|
||||
end
|
||||
|
||||
test do
|
||||
# Ensure pkg-config can find the expected module name used by Strawberry.
|
||||
assert_match "libgpod", shell_output("pkg-config --libs libgpod-1.0")
|
||||
end
|
||||
end
|
||||
|
||||
8
Formula/libgpod/README.md
Normal file
8
Formula/libgpod/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# libgpod (local Homebrew formula)
|
||||
|
||||
Homebrew core does not currently ship `libgpod`, but Strawberry can optionally use it
|
||||
to support **classic iPod** devices (via `libgpod-1.0` + `gdk-pixbuf-2.0`).
|
||||
|
||||
This formula is pinned to a known-good upstream snapshot and disables Linux-specific
|
||||
integration (udev/HAL) and language bindings to keep the build reliable on macOS.
|
||||
|
||||
93
Formula/macdeploycheck.rb
Normal file
93
Formula/macdeploycheck.rb
Normal file
@@ -0,0 +1,93 @@
|
||||
class Macdeploycheck < Formula
|
||||
desc "Sanity checks a macOS .app bundle for accidental Homebrew runtime dependencies"
|
||||
homepage "https://github.com/strawberrymusicplayer/strawberry"
|
||||
url "file://#{__FILE__}"
|
||||
version "0.1.0"
|
||||
sha256 :no_check
|
||||
license "MIT"
|
||||
|
||||
depends_on :macos
|
||||
|
||||
def install
|
||||
(bin/"macdeploycheck").write <<~'EOS'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
app="${1:-}"
|
||||
if [[ -z "$app" ]]; then
|
||||
echo "Usage: macdeploycheck <path/to/App.app>" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "$app" ]]; then
|
||||
echo "Error: app bundle not found: $app" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "$app/Contents" ]]; then
|
||||
echo "Error: not a macOS app bundle (missing Contents/): $app" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
fail=0
|
||||
tmp="$(mktemp -t macdeploycheck.XXXXXX)"
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
|
||||
# Collect Mach-O files (executables + dylibs) inside the bundle.
|
||||
while IFS= read -r -d '' f; do
|
||||
if file "$f" | grep -q "Mach-O"; then
|
||||
echo "$f" >>"$tmp"
|
||||
fi
|
||||
done < <(find "$app/Contents" -type f \( -perm -111 -o -name "*.dylib" -o -name "*.so" \) -print0 2>/dev/null)
|
||||
|
||||
if [[ ! -s "$tmp" ]]; then
|
||||
echo "Warning: no Mach-O files found under $app/Contents" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "macdeploycheck: scanning for external (Homebrew) runtime deps..."
|
||||
while IFS= read -r f; do
|
||||
# otool -L prints:
|
||||
# <file>:
|
||||
# <dep> (compatibility version ..., current version ...)
|
||||
deps="$(otool -L "$f" 2>/dev/null | tail -n +2 | awk '{print $1}' || true)"
|
||||
while IFS= read -r dep; do
|
||||
[[ -z "$dep" ]] && continue
|
||||
|
||||
# Ignore system and rpath/loader/executable paths.
|
||||
case "$dep" in
|
||||
/System/*|/usr/lib/*|@rpath/*|@loader_path/*|@executable_path/*) continue ;;
|
||||
esac
|
||||
|
||||
# These are the common accidental runtime deps that will break distribution.
|
||||
if [[ "$dep" == /opt/homebrew/* || "$dep" == /usr/local/* || "$dep" == /opt/local/* ]]; then
|
||||
echo "ERROR: $f links to external path: $dep" >&2
|
||||
fail=1
|
||||
fi
|
||||
done <<<"$deps"
|
||||
done <"$tmp"
|
||||
|
||||
if [[ "$fail" -ne 0 ]]; then
|
||||
cat >&2 <<'EOM'
|
||||
|
||||
One or more binaries in your .app link to a Homebrew (or MacPorts) path.
|
||||
That usually means the bundle is not self-contained and will fail on other machines,
|
||||
or will fail notarization/codesigning validation.
|
||||
|
||||
Fix: re-run your deploy step (e.g. macdeployqt) so frameworks/dylibs are bundled and
|
||||
their install names are rewritten to @rpath/@loader_path.
|
||||
EOM
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: no external Homebrew/MacPorts runtime deps detected."
|
||||
exit 0
|
||||
EOS
|
||||
|
||||
chmod 0755, bin/"macdeploycheck"
|
||||
end
|
||||
|
||||
test do
|
||||
# Basic smoke test: tool runs and prints usage.
|
||||
system bin/"macdeploycheck"
|
||||
end
|
||||
end
|
||||
|
||||
36
Formula/macdeploycheck/README.md
Normal file
36
Formula/macdeploycheck/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# `macdeploycheck` (local Homebrew formula)
|
||||
|
||||
This repository includes a small helper tool called `macdeploycheck`, packaged as a local Homebrew formula.
|
||||
|
||||
## What it does
|
||||
|
||||
`macdeploycheck` scans a built `.app` bundle and flags common **accidental runtime dependencies** on:
|
||||
|
||||
- Homebrew paths like `/opt/homebrew/...` or `/usr/local/...`
|
||||
- MacPorts paths like `/opt/local/...`
|
||||
|
||||
These dependencies usually mean the `.app` is **not self-contained** and may fail to run on other machines or fail notarization validation.
|
||||
|
||||
## Install (via this repo's tap)
|
||||
|
||||
From the Strawberry repo root:
|
||||
|
||||
```bash
|
||||
brew tap strawberry/local "file://$PWD"
|
||||
brew install strawberry/local/macdeploycheck
|
||||
```
|
||||
|
||||
Or use the repo `Brewfile`:
|
||||
|
||||
```bash
|
||||
brew bundle --file Brewfile
|
||||
```
|
||||
|
||||
## Use
|
||||
|
||||
```bash
|
||||
macdeploycheck /path/to/Strawberry.app
|
||||
```
|
||||
|
||||
It exits non-zero if it finds external runtime deps.
|
||||
|
||||
45
Formula/qtsparkle-qt6.rb
Normal file
45
Formula/qtsparkle-qt6.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
class QtsparkleQt6 < Formula
|
||||
desc "Qt wrapper library for in-app updates (Qt 6 build)"
|
||||
homepage "https://github.com/strawberrymusicplayer/qtsparkle"
|
||||
url "https://github.com/strawberrymusicplayer/qtsparkle/archive/95ca3b77a79540d632b29e9a4df9aed30af5f901.tar.gz"
|
||||
sha256 "945c9e96d2f6175b134a8ccfd6ec1acd268266d31969b5870d4037e8e5877834"
|
||||
license "GPL-3.0-or-later"
|
||||
|
||||
depends_on "cmake" => :build
|
||||
depends_on "ninja" => :build
|
||||
depends_on "qt"
|
||||
|
||||
def install
|
||||
args = std_cmake_args + %W[
|
||||
-GNinja
|
||||
-DBUILD_WITH_QT6=ON
|
||||
-DBUILD_WITH_QT5=OFF
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
-DBUILD_STATIC_LIBS=OFF
|
||||
]
|
||||
|
||||
system "cmake", "-S", ".", "-B", "build", *args
|
||||
system "cmake", "--build", "build"
|
||||
system "cmake", "--install", "build"
|
||||
end
|
||||
|
||||
test do
|
||||
# Strawberry expects: find_package(qtsparkle-qt6) and target qtsparkle-qt6::qtsparkle
|
||||
(testpath/"CMakeLists.txt").write <<~CMAKE
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(qtsparkle_test LANGUAGES CXX)
|
||||
find_package(qtsparkle-qt6 CONFIG REQUIRED)
|
||||
add_library(dummy STATIC dummy.cpp)
|
||||
target_link_libraries(dummy PRIVATE qtsparkle-qt6::qtsparkle)
|
||||
CMAKE
|
||||
|
||||
(testpath/"dummy.cpp").write <<~CPP
|
||||
int dummy() { return 0; }
|
||||
CPP
|
||||
|
||||
system "cmake", "-S", ".", "-B", "build",
|
||||
"-DCMAKE_PREFIX_PATH=#{opt_prefix}"
|
||||
system "cmake", "--build", "build"
|
||||
end
|
||||
end
|
||||
|
||||
10
Formula/qtsparkle-qt6/README.md
Normal file
10
Formula/qtsparkle-qt6/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# qtsparkle-qt6 (local Homebrew formula)
|
||||
|
||||
This installs Strawberry’s Qt updater helper library as a CMake package:
|
||||
|
||||
- `find_package(qtsparkle-qt6 CONFIG REQUIRED)`
|
||||
- target: `qtsparkle-qt6::qtsparkle`
|
||||
|
||||
Strawberry will pick it up automatically when present and enable the optional
|
||||
**QtSparkle integration**.
|
||||
|
||||
19
Formula/sparkle-framework.rb
Normal file
19
Formula/sparkle-framework.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
class SparkleFramework < Formula
|
||||
desc "Sparkle.framework for macOS app updates (framework-only packaging)"
|
||||
homepage "https://sparkle-project.org/"
|
||||
url "https://github.com/sparkle-project/Sparkle/releases/download/2.8.1/Sparkle-2.8.1.tar.xz"
|
||||
sha256 "5cddb7695674ef7704268f38eccaee80e3accbf19e61c1689efff5b6116d85be"
|
||||
license "MIT"
|
||||
|
||||
depends_on :macos
|
||||
|
||||
def install
|
||||
frameworks = prefix/"Frameworks"
|
||||
frameworks.install "Sparkle.framework"
|
||||
end
|
||||
|
||||
test do
|
||||
assert_predicate prefix/"Frameworks/Sparkle.framework", :exist?
|
||||
end
|
||||
end
|
||||
|
||||
9
Formula/sparkle-framework/README.md
Normal file
9
Formula/sparkle-framework/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# sparkle-framework (local Homebrew formula)
|
||||
|
||||
Installs the upstream `Sparkle.framework` into:
|
||||
|
||||
- `$(brew --prefix sparkle-framework)/Frameworks/Sparkle.framework`
|
||||
|
||||
This is used to enable Strawberry’s optional **Sparkle integration** on macOS
|
||||
(`find_library(SPARKLE Sparkle)` in the main `CMakeLists.txt`).
|
||||
|
||||
169
README.md
169
README.md
@@ -1,118 +1,121 @@
|
||||
:strawberry: Strawberry Music Player [](https://github.com/strawberrymusicplayer/strawberry/actions)
|
||||
=======================
|
||||
[](https://github.com/sponsors/jonaski)
|
||||
[](https://patreon.com/jonaskvinge)
|
||||
[](https://paypal.me/jonaskvinge)
|
||||
# Strawberry (macOS-focused fork)
|
||||
|
||||
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.
|
||||
This repository is a **macOS-focused fork** of the upstream Strawberry Music Player project originally created and maintained by **Jonas Kvinge** (itself originally forked from Clementine).
|
||||
|
||||

|
||||
The goal of this fork is to make Strawberry **build cleanly and repeatably on macOS**, with:
|
||||
|
||||
Resources:
|
||||
- Homebrew dependency installation via `Brewfile`
|
||||
- local Homebrew formulas (tap) for missing dependencies
|
||||
- build / deploy / signing / notarization helper scripts under `build_tools/`
|
||||
- Sparkle feed configuration knobs so you can publish your own updates
|
||||
|
||||
* Website: https://www.strawberrymusicplayer.org/
|
||||
* Wiki: https://wiki.strawberrymusicplayer.org/
|
||||
* Forum: https://forum.strawberrymusicplayer.org/
|
||||
* Github: https://github.com/strawberrymusicplayer/strawberry
|
||||
* Latest builds: https://builds.strawberrymusicplayer.org/
|
||||
* openSUSE buildservice: https://build.opensuse.org/package/show/home:jonaski:audio/strawberry
|
||||
* Ubuntu PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||
* Ubuntu Unstable PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
|
||||
* Translations: https://crowdin.com/project/strawberrymusicplayer/
|
||||
## Upstream vs this fork (macOS distribution)
|
||||
|
||||
### :bangbang: Opening an issue
|
||||
Upstream Strawberry is free/open-source, but upstream’s **macOS binaries** may be distributed via a **paid-access channel** depending on the release period and policy.
|
||||
|
||||
* 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/
|
||||
This fork is intended for people who want to:
|
||||
|
||||
### :moneybag: Sponsoring
|
||||
- **build from source on macOS** without guesswork
|
||||
- **produce signed + notarized binaries** themselves (and optionally distribute them)
|
||||
|
||||
The program is free software, released under GPL. If you like this program and can make use of it, consider sponsoring or donating to help fund the project.
|
||||
There are currently 4 options for sponsoring:
|
||||
General safety note: whether you use upstream builds, your own builds, or someone else’s, only install software from sources you trust and prefer **signed + notarized** releases.
|
||||
|
||||
1. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||
2. [GitHub](https://github.com/sponsors/jonaski)
|
||||
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
||||
4. [PayPal](https://paypal.me/jonaskvinge)
|
||||
## Quick start (macOS)
|
||||
|
||||
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.
|
||||
Install Homebrew dependencies:
|
||||
|
||||
### :heavy_check_mark: Features
|
||||
```bash
|
||||
./build_tools/macos/install_brew_deps.sh
|
||||
```
|
||||
|
||||
* Play and organize music
|
||||
* Supports WAV, FLAC, WavPack, Ogg FLAC, Ogg Vorbis, Ogg Opus, Ogg Speex, MPC, TrueAudio, AIFF, MP4, MP3, ASF and Monkey's Audio.
|
||||
* Audio CD playback
|
||||
* Native desktop notifications
|
||||
* Playlist management
|
||||
* Smart and dynamic playlists
|
||||
* Advanced audio output and device configuration for bit-perfect playback on Linux
|
||||
* In-player song loudness analysis and song playback loudness normalization, as per EBU R 128
|
||||
* Edit tags on audio files
|
||||
* Fetch tags from MusicBrainz
|
||||
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
|
||||
* Song lyrics from [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
|
||||
Build:
|
||||
|
||||
```bash
|
||||
./build_tools/macos/build_app.sh --release --clean
|
||||
open ./cmake-build-macos-release/strawberry.app
|
||||
```
|
||||
|
||||
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
|
||||
Build + deploy + sign + notarize (+ DMG):
|
||||
|
||||
**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.**
|
||||
```bash
|
||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy --dmg \
|
||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
||||
--notary-profile "<profile-name>"
|
||||
```
|
||||
|
||||
### :heavy_exclamation_mark: Requirements
|
||||
## Features
|
||||
|
||||
To build Strawberry from source you need the following installed on your system with the additional development packages/headers:
|
||||
- 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) and [lrclib.net](https://lrclib.net/)
|
||||
- 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
|
||||
|
||||
* [CMake 3.13 or higher](https://cmake.org/)
|
||||
* C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/) or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
|
||||
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
||||
* [Boost](https://www.boost.org/)
|
||||
* [GLib](https://developer.gnome.org/glib/)
|
||||
* [Qt 6.4.0 or higher with components Core, Concurrent, Gui, Widgets, Network, Sql and D-Bus](https://www.qt.io/)
|
||||
* [SQLite 3.9 or newer](https://www.sqlite.org)
|
||||
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
|
||||
* [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
* [TagLib 1.12 or higher](https://www.taglib.org/)
|
||||
* [ICU](https://unicode-org.github.io/icu/)
|
||||
* [KDSingleApplication 1.1.0 or higher](https://github.com/KDAB/KDSingleApplication)
|
||||
---
|
||||
|
||||
Optional dependencies:
|
||||
:white_check_mark: Tested on **Linux**, **OpenBSD**, **FreeBSD**, **macOS**, and **Windows**.
|
||||
|
||||
* Song fingerprinting and MusicBrainz tagging: [Chromaprint](https://acoustid.org/chromaprint)
|
||||
* Moodbar: [fftw3](http://www.fftw.org/)
|
||||
* PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/?)
|
||||
* Audio CD: [libcdio](https://www.gnu.org/software/libcdio/)
|
||||
* MTP devices: [libmtp](http://libmtp.sourceforge.net/)
|
||||
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
|
||||
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
|
||||
* Discord rich presence [RapidJSON](https://rapidjson.org/)
|
||||
## :gear: Requirements
|
||||
|
||||
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.
|
||||
To build Strawberry from source, you’ll need:
|
||||
|
||||
### :wrench: Build from source
|
||||
**Dependencies:**
|
||||
- [CMake ≥= 3.13](https://cmake.org/)
|
||||
- C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/), or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
|
||||
- [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
||||
- [Boost](https://www.boost.org/)
|
||||
- [GLib](https://developer.gnome.org/glib/)
|
||||
- [Qt ≥= 6.4](https://www.qt.io/) (Core, Concurrent, Gui, Widgets, Network, SQL, D-Bus)
|
||||
- [SQLite ≥= 3.9](https://www.sqlite.org)
|
||||
- [ALSA (Linux only)](https://www.alsa-project.org/)
|
||||
- [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
- [TagLib ≥= 1.12](https://www.taglib.org/)
|
||||
- [ICU](https://unicode-org.github.io/icu/)
|
||||
- [KDSingleApplication ≥= 1.1.0](https://github.com/KDAB/KDSingleApplication)
|
||||
|
||||
### Get the code:
|
||||
**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/)
|
||||
|
||||
Also install GStreamer plugins **base**, **good**, and optionally **bad**, **ugly** and **libav** for full codec support.
|
||||
|
||||
---
|
||||
|
||||
## :wrench: Build from Source
|
||||
|
||||
**Get the code:**
|
||||
|
||||
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
|
||||
|
||||
### Build and install:
|
||||
**Build and install:**
|
||||
|
||||
cd strawberry
|
||||
cmake -S . -B build
|
||||
cmake --build build --parallel $(nproc)
|
||||
sudo cmake --install build
|
||||
|
||||
To build on Windows with Visual Studio 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
For building on Windows with Visual Studio 2022, see: :point_right: https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
|
||||
### :penguin: Packaging status
|
||||
---
|
||||
|
||||
## :package: Packaging status
|
||||
|
||||
[](https://repology.org/metapackage/strawberry/versions)
|
||||
|
||||
79
build_tools/README.md
Normal file
79
build_tools/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Build helper scripts
|
||||
|
||||
This `build_tools/` directory contains **helper scripts and notes** for building Strawberry.
|
||||
|
||||
- It is **not** intended to be your CMake build output directory.
|
||||
- Recommended CMake build output directories: `cmake-build/`, `build-release/`, etc.
|
||||
|
||||
## macOS
|
||||
|
||||
- Install dependencies via Homebrew:
|
||||
|
||||
```bash
|
||||
./build_tools/macos/install_brew_deps.sh
|
||||
```
|
||||
|
||||
- Build Strawberry:
|
||||
|
||||
```bash
|
||||
./build_tools/macos/build_app.sh --release
|
||||
open ./cmake-build-macos-release/strawberry.app
|
||||
```
|
||||
|
||||
## macOS signing + notarization (Developer ID distribution)
|
||||
|
||||
This repo includes `build_tools/macos/build_sign_notarize.sh` to automate:
|
||||
|
||||
- build → (optional deploy) → codesign → notarize → staple → verify
|
||||
|
||||
### One-time setup (Apple Developer)
|
||||
|
||||
- **Install certificates**:
|
||||
- In the Apple Developer portal, create (or download) a **Developer ID Application** certificate.
|
||||
- Install it into your login keychain (Xcode can manage this via **Xcode → Settings → Accounts**).
|
||||
|
||||
- **Provisioning profiles**:
|
||||
- For **Developer ID distribution (outside the Mac App Store)**, you typically **do not need a provisioning profile**.
|
||||
- You *do* need profiles if you are building a **Mac App Store**-signed app (not what this repo’s scripts target).
|
||||
|
||||
- **Notarization credentials**:
|
||||
- Create a `notarytool` keychain profile (recommended) so you don’t have to pass secrets on the command line:
|
||||
|
||||
```bash
|
||||
# NOTE: <profile-name> is a positional argument (not a flag).
|
||||
# Pick any name you want, e.g. "strawberry-notary".
|
||||
xcrun notarytool store-credentials "<profile-name>" \
|
||||
--apple-id "<your-apple-id>" \
|
||||
--team-id "<TEAMID>" \
|
||||
--password "<app-specific-password>"
|
||||
```
|
||||
|
||||
### Listing what’s installed locally
|
||||
|
||||
Run with no args to list local signing identities + notarytool profiles:
|
||||
|
||||
```bash
|
||||
./build_tools/macos/build_sign_notarize.sh
|
||||
```
|
||||
|
||||
### Build + sign + notarize
|
||||
|
||||
```bash
|
||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy \
|
||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
||||
--notary-profile "<profile-name>"
|
||||
```
|
||||
|
||||
### Build + sign + notarize + DMG (recommended for public distribution)
|
||||
|
||||
This produces:
|
||||
|
||||
- a notarized `strawberry.app` (stapled)
|
||||
- a notarized `strawberry-notarize.zip` (useful for Sparkle / downloads)
|
||||
- a notarized `strawberry-*.dmg` (stapled)
|
||||
|
||||
```bash
|
||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy --dmg \
|
||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
||||
--notary-profile "<profile-name>"
|
||||
```
|
||||
266
build_tools/macos/build_app.sh
Executable file
266
build_tools/macos/build_app.sh
Executable file
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ts() { date +"%H:%M:%S"; }
|
||||
lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; }
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
||||
|
||||
current_cmd_pid=""
|
||||
current_hb_pid=""
|
||||
|
||||
kill_tree() {
|
||||
local pid="$1"
|
||||
[[ -z "${pid}" ]] && return 0
|
||||
# Recurse into children first (best-effort).
|
||||
local child
|
||||
for child in $(pgrep -P "$pid" 2>/dev/null || true); do
|
||||
kill_tree "$child"
|
||||
done
|
||||
kill -TERM "$pid" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
# Never fail cleanup on errors.
|
||||
set +e
|
||||
if [[ -n "${current_hb_pid}" ]]; then
|
||||
kill "${current_hb_pid}" >/dev/null 2>&1 || true
|
||||
wait "${current_hb_pid}" >/dev/null 2>&1 || true
|
||||
current_hb_pid=""
|
||||
fi
|
||||
if [[ -n "${current_cmd_pid}" ]]; then
|
||||
# If still running, terminate process tree.
|
||||
kill -0 "${current_cmd_pid}" >/dev/null 2>&1 && kill_tree "${current_cmd_pid}"
|
||||
current_cmd_pid=""
|
||||
fi
|
||||
}
|
||||
|
||||
trap 'cleanup; exit 130' INT TERM
|
||||
trap 'cleanup' EXIT
|
||||
|
||||
run_with_heartbeat() {
|
||||
local desc="$1"
|
||||
shift
|
||||
|
||||
local start now elapsed hb_pid
|
||||
start="$(date +%s)"
|
||||
|
||||
echo "==> [$(ts)] ${desc}"
|
||||
|
||||
# Run the command in the background so we can reliably clean it up on Ctrl-C.
|
||||
set +e
|
||||
"$@" &
|
||||
local cmd_pid=$!
|
||||
set -e
|
||||
current_cmd_pid="$cmd_pid"
|
||||
|
||||
(
|
||||
while kill -0 "$cmd_pid" >/dev/null 2>&1; do
|
||||
sleep 20
|
||||
now="$(date +%s)"
|
||||
elapsed="$((now - start))"
|
||||
echo " [$(ts)] ... still working (${elapsed}s elapsed) ..."
|
||||
done
|
||||
) &
|
||||
hb_pid="$!"
|
||||
current_hb_pid="$hb_pid"
|
||||
|
||||
set +e
|
||||
wait "$cmd_pid"
|
||||
local rc=$?
|
||||
set -e
|
||||
|
||||
# Clear globals before stopping heartbeat to avoid cleanup double-kill.
|
||||
current_cmd_pid=""
|
||||
kill "$hb_pid" >/dev/null 2>&1 || true
|
||||
wait "$hb_pid" >/dev/null 2>&1 || true
|
||||
current_hb_pid=""
|
||||
|
||||
now="$(date +%s)"
|
||||
elapsed="$((now - start))"
|
||||
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
echo "Error: '${desc}' failed after ${elapsed}s (exit $rc)." >&2
|
||||
return "$rc"
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Done: ${desc} (${elapsed}s)"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./build_tools/macos/build_app.sh [--debug|--release] [--deploy] [--dmg] [--clean] [--build-dir <path>]
|
||||
|
||||
What it does:
|
||||
- Configures and builds Strawberry with CMake + Ninja
|
||||
- Optional: runs CMake targets 'deploy' (bundle deps) and 'dmg' (create DMG)
|
||||
|
||||
Options:
|
||||
--release Release build (default)
|
||||
--debug Debug build
|
||||
--deploy Run: cmake --build <builddir> --target deploy
|
||||
--dmg Run: cmake --build <builddir> --target dmg (implies --deploy)
|
||||
--clean Delete the build dir before configuring
|
||||
--build-dir Override build directory (default: <repo>/cmake-build-macos-<config>)
|
||||
-h, --help Show help
|
||||
EOF
|
||||
}
|
||||
|
||||
config="Release"
|
||||
do_deploy=0
|
||||
do_dmg=0
|
||||
do_clean=0
|
||||
build_dir=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--release) config="Release"; shift ;;
|
||||
--debug) config="Debug"; shift ;;
|
||||
--deploy) do_deploy=1; shift ;;
|
||||
--dmg) do_dmg=1; do_deploy=1; shift ;;
|
||||
--clean) do_clean=1; shift ;;
|
||||
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
echo "Error: This script is for macOS only." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then
|
||||
echo "Error: Xcode Command Line Tools not found." >&2
|
||||
echo "Install them first: xcode-select --install" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
echo "Error: Homebrew ('brew') not found in PATH." >&2
|
||||
echo "Install Homebrew first: https://brew.sh/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v cmake >/dev/null 2>&1; then
|
||||
echo "Error: cmake not found. Did you run ./build_tools/macos/install_brew_deps.sh ?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v ninja >/dev/null 2>&1; then
|
||||
echo "Error: ninja not found. Did you run ./build_tools/macos/install_brew_deps.sh ?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
brew_prefix="$(brew --prefix)"
|
||||
qt_prefix="$(brew --prefix qt)"
|
||||
icu_prefix="$(brew --prefix icu4c || true)"
|
||||
|
||||
if [[ -z "$build_dir" ]]; then
|
||||
build_dir="${repo_root}/cmake-build-macos-$(lower "$config")"
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Repo: ${repo_root}"
|
||||
echo "==> [$(ts)] Build dir: ${build_dir}"
|
||||
echo "==> [$(ts)] Config: ${config}"
|
||||
|
||||
if [[ "$do_clean" -eq 1 ]]; then
|
||||
echo "==> [$(ts)] Cleaning build dir"
|
||||
# macOS 26+ can apply provenance metadata that blocks deletion even when permissions look normal.
|
||||
# Clear common xattrs and immutable flags before deleting.
|
||||
xattr -dr com.apple.provenance "$build_dir" >/dev/null 2>&1 || true
|
||||
xattr -dr com.apple.quarantine "$build_dir" >/dev/null 2>&1 || true
|
||||
chflags -R nouchg,noschg "$build_dir" >/dev/null 2>&1 || true
|
||||
rm -rf "$build_dir" || {
|
||||
echo "Error: failed to remove build dir: $build_dir" >&2
|
||||
echo "This is usually due to macOS provenance/flags. Try:" >&2
|
||||
echo " xattr -dr com.apple.provenance \"$build_dir\"" >&2
|
||||
echo " chflags -R nouchg,noschg \"$build_dir\"" >&2
|
||||
echo " rm -rf \"$build_dir\"" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
mkdir -p "$build_dir"
|
||||
|
||||
# If you've run a previously-built app directly from the build directory, macOS can apply provenance
|
||||
# metadata that makes the bundle effectively immutable (even when permissions look normal).
|
||||
# That breaks CMake because it needs to update strawberry.app/Contents/Info.plist during configure/build.
|
||||
app_bundle="${build_dir}/strawberry.app"
|
||||
if [[ -d "${app_bundle}/Contents" ]]; then
|
||||
# Try to clear provenance/quarantine metadata first (best effort).
|
||||
xattr -dr com.apple.provenance "${app_bundle}" >/dev/null 2>&1 || true
|
||||
xattr -dr com.apple.quarantine "${app_bundle}" >/dev/null 2>&1 || true
|
||||
|
||||
# If the bundle is still not writable, remove it so CMake can recreate it.
|
||||
if ! ( : > "${app_bundle}/Contents/.cmake_write_test" ) 2>/dev/null; then
|
||||
echo "==> [$(ts)] Existing ${app_bundle} is not writable (likely macOS provenance). Removing it."
|
||||
rm -rf "${app_bundle}"
|
||||
else
|
||||
rm -f "${app_bundle}/Contents/.cmake_write_test" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make pkg-config more reliable with Homebrew.
|
||||
export PKG_CONFIG_PATH="${brew_prefix}/lib/pkgconfig:${brew_prefix}/share/pkgconfig:${PKG_CONFIG_PATH:-}"
|
||||
|
||||
# For dist/CMakeLists.txt Info.plist minimum version logic.
|
||||
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-12.0}"
|
||||
|
||||
cmake_prefix_path="${qt_prefix};${brew_prefix}"
|
||||
|
||||
cmake_extra_args=()
|
||||
|
||||
# Optional: override Sparkle update feed / key for your own published builds.
|
||||
# Example:
|
||||
# export SPARKLE_FEED_URL="https://example.com/appcast.xml"
|
||||
# export SPARKLE_PUBLIC_ED25519_KEY="base64=="
|
||||
if [[ -n "${SPARKLE_FEED_URL:-}" ]]; then
|
||||
cmake_extra_args+=("-DSPARKLE_FEED_URL=${SPARKLE_FEED_URL}")
|
||||
fi
|
||||
if [[ -n "${SPARKLE_PUBLIC_ED25519_KEY:-}" ]]; then
|
||||
cmake_extra_args+=("-DSPARKLE_PUBLIC_ED25519_KEY=${SPARKLE_PUBLIC_ED25519_KEY}")
|
||||
fi
|
||||
|
||||
run_with_heartbeat "Configuring (CMAKE_PREFIX_PATH=${cmake_prefix_path})" \
|
||||
cmake -S "$repo_root" -B "$build_dir" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE="$config" \
|
||||
-DCMAKE_PREFIX_PATH="$cmake_prefix_path" \
|
||||
-DCMAKE_FRAMEWORK_PATH="${brew_prefix}/Frameworks;${brew_prefix}/opt/sparkle-framework/Frameworks" \
|
||||
-DOPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL=OFF \
|
||||
${cmake_extra_args+"${cmake_extra_args[@]}"} \
|
||||
${icu_prefix:+-DICU_ROOT="$icu_prefix"}
|
||||
|
||||
run_with_heartbeat "Building" \
|
||||
cmake --build "$build_dir" --parallel
|
||||
|
||||
if [[ "$do_deploy" -eq 1 ]]; then
|
||||
echo "==> [$(ts)] Preparing env for 'deploy' target (GIO/GStreamer)"
|
||||
export GIO_EXTRA_MODULES="${brew_prefix}/lib/gio/modules"
|
||||
export GST_PLUGIN_SCANNER="${brew_prefix}/opt/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner"
|
||||
export GST_PLUGIN_PATH="${brew_prefix}/lib/gstreamer-1.0"
|
||||
|
||||
# Optional, but helps dist/macos/macgstcopy.sh bundle libsoup which GStreamer loads dynamically.
|
||||
libsoup_prefix="$(brew --prefix libsoup 2>/dev/null || true)"
|
||||
if [[ -n "${libsoup_prefix}" ]]; then
|
||||
libsoup_dylib="$(ls -1 "${libsoup_prefix}"/lib/libsoup-*.dylib 2>/dev/null | head -n 1 || true)"
|
||||
if [[ -n "${libsoup_dylib}" ]]; then
|
||||
export LIBSOUP_LIBRARY_PATH="${libsoup_dylib}"
|
||||
fi
|
||||
fi
|
||||
|
||||
run_with_heartbeat "Running: deploy" \
|
||||
cmake --build "$build_dir" --target deploy
|
||||
fi
|
||||
|
||||
if [[ "$do_dmg" -eq 1 ]]; then
|
||||
run_with_heartbeat "Running: dmg" \
|
||||
cmake --build "$build_dir" --target dmg
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Done"
|
||||
echo "Built app:"
|
||||
echo " ${build_dir}/strawberry.app"
|
||||
|
||||
309
build_tools/macos/build_sign_notarize.sh
Executable file
309
build_tools/macos/build_sign_notarize.sh
Executable file
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ts() { date +"%H:%M:%S"; }
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./build_tools/macos/build_sign_notarize.sh # list local signing identities + notary profiles
|
||||
./build_tools/macos/build_sign_notarize.sh --run [options] # build, sign, notarize, staple
|
||||
|
||||
Common options:
|
||||
--run Perform build/sign/notarize (otherwise list identities/profiles)
|
||||
--release | --debug Build config (default: Release)
|
||||
--clean Clean build dir before build
|
||||
--deploy Run CMake 'deploy' target before signing (default: on)
|
||||
--no-deploy Do not run 'deploy' (not recommended for distribution)
|
||||
--dmg Build a DMG after app notarization, then notarize+staple the DMG too
|
||||
--build-dir <path> Override build directory
|
||||
|
||||
Signing options:
|
||||
--identity "<name>" Codesign identity (e.g. "Developer ID Application: Your Name (TEAMID)")
|
||||
--entitlements <plist> Optional entitlements plist for codesign
|
||||
|
||||
Notarization options (recommended):
|
||||
--notary-profile <name> notarytool keychain profile name (created via `xcrun notarytool store-credentials <name> ...`)
|
||||
--skip-notarize Skip notarization
|
||||
|
||||
Outputs:
|
||||
- Signed app: <build-dir>/strawberry.app
|
||||
- Zip for notarization: <build-dir>/strawberry-notarize.zip
|
||||
- DMG (optional): <build-dir>/strawberry-*.dmg
|
||||
|
||||
Notes:
|
||||
- This script is intended for Developer ID distribution (outside Mac App Store).
|
||||
- If you want Sparkle updates, you'll typically ship a notarized .zip + an appcast feed.
|
||||
EOF
|
||||
}
|
||||
|
||||
list_identities_and_profiles() {
|
||||
echo "==> [$(ts)] macOS code signing identities (Keychain)"
|
||||
security find-identity -p codesigning -v || true
|
||||
|
||||
echo
|
||||
echo "==> [$(ts)] notarytool credential profiles"
|
||||
echo "Note: this Xcode notarytool version does not provide a 'list-profiles' command."
|
||||
echo "If you forgot the profile name you created, check Keychain Access or re-run:"
|
||||
echo " xcrun notarytool store-credentials \"<profile-name>\" --apple-id \"you@example.com\" --team-id \"TEAMID\""
|
||||
|
||||
echo
|
||||
echo "==> [$(ts)] Provisioning profiles (macOS)"
|
||||
prof_dir="${HOME}/Library/MobileDevice/Provisioning Profiles"
|
||||
if [[ -d "${prof_dir}" ]]; then
|
||||
ls -la "${prof_dir}" | head -n 50
|
||||
else
|
||||
echo "(none found at '${prof_dir}')"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
echo "Error: This script is for macOS only." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then
|
||||
echo "Error: Xcode Command Line Tools not found." >&2
|
||||
echo "Install them first: xcode-select --install" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
do_run=0
|
||||
config="Release"
|
||||
do_clean=0
|
||||
do_deploy=1
|
||||
do_dmg=0
|
||||
build_dir=""
|
||||
identity=""
|
||||
entitlements=""
|
||||
notary_profile=""
|
||||
skip_notarize=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--run) do_run=1; shift ;;
|
||||
--release) config="Release"; shift ;;
|
||||
--debug) config="Debug"; shift ;;
|
||||
--clean) do_clean=1; shift ;;
|
||||
--deploy) do_deploy=1; shift ;;
|
||||
--no-deploy) do_deploy=0; shift ;;
|
||||
--dmg) do_dmg=1; shift ;;
|
||||
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
||||
--identity) identity="${2:-}"; shift 2 ;;
|
||||
--entitlements) entitlements="${2:-}"; shift 2 ;;
|
||||
--notary-profile) notary_profile="${2:-}"; shift 2 ;;
|
||||
--skip-notarize) skip_notarize=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$do_run" -eq 0 ]]; then
|
||||
usage
|
||||
echo
|
||||
list_identities_and_profiles
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$build_dir" ]]; then
|
||||
lc_config="$(echo "$config" | tr '[:upper:]' '[:lower:]')"
|
||||
build_dir="${repo_root}/cmake-build-macos-${lc_config}"
|
||||
fi
|
||||
|
||||
app_path="${build_dir}/strawberry.app"
|
||||
bin_path="${app_path}/Contents/MacOS/strawberry"
|
||||
zip_path="${build_dir}/strawberry-notarize.zip"
|
||||
dmg_path=""
|
||||
|
||||
notarize_and_maybe_staple() {
|
||||
local file_path="$1"
|
||||
local label="$2"
|
||||
local do_staple="${3:-1}"
|
||||
|
||||
echo "==> [$(ts)] Notarizing ${label}"
|
||||
local out
|
||||
out="$(mktemp -t notarytool-submit.XXXXXX)"
|
||||
xcrun notarytool submit "$file_path" --keychain-profile "$notary_profile" --wait --output-format plist --no-progress >"$out"
|
||||
|
||||
local submit_id submit_status
|
||||
submit_id="$(/usr/bin/plutil -extract id raw -o - "$out" 2>/dev/null || true)"
|
||||
submit_status="$(/usr/bin/plutil -extract status raw -o - "$out" 2>/dev/null || true)"
|
||||
rm -f "$out" || true
|
||||
|
||||
if [[ -z "$submit_id" ]]; then
|
||||
echo "Error: could not parse notarization submission id for ${label}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Notary submission id: $submit_id"
|
||||
echo "==> [$(ts)] Notary status: $submit_status"
|
||||
|
||||
if [[ "$submit_status" != "Accepted" ]]; then
|
||||
echo "Error: notarization failed for ${label} with status '$submit_status'. Fetching log..." >&2
|
||||
xcrun notarytool log "$submit_id" --keychain-profile "$notary_profile" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$do_staple" -eq 1 ]]; then
|
||||
echo "==> [$(ts)] Stapling ${label}"
|
||||
xcrun stapler staple "$file_path"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -z "$identity" ]]; then
|
||||
echo "Error: Missing --identity (Developer ID Application identity)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$skip_notarize" -eq 0 && -z "$notary_profile" ]]; then
|
||||
echo "Error: Missing --notary-profile (or pass --skip-notarize)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Building Strawberry"
|
||||
build_args=( "--release" )
|
||||
if [[ "$config" == "Debug" ]]; then build_args=( "--debug" ); fi
|
||||
if [[ "$do_clean" -eq 1 ]]; then build_args+=( "--clean" ); fi
|
||||
if [[ -n "$build_dir" ]]; then build_args+=( "--build-dir" "$build_dir" ); fi
|
||||
if [[ "$do_deploy" -eq 1 ]]; then build_args+=( "--deploy" ); fi
|
||||
|
||||
"${repo_root}/build_tools/macos/build_app.sh" "${build_args[@]}"
|
||||
|
||||
if [[ ! -x "$bin_path" ]]; then
|
||||
echo "Error: built app not found at: $app_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Codesigning (hardened runtime)"
|
||||
codesign_args=( --force --timestamp --options runtime --sign "$identity" )
|
||||
if [[ -n "$entitlements" ]]; then
|
||||
codesign_args+=( --entitlements "$entitlements" )
|
||||
fi
|
||||
|
||||
# Clean up any leftover codesign temp files from previous interrupted runs.
|
||||
# codesign may create *.cstemp alongside binaries while updating signatures.
|
||||
find "$app_path" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do
|
||||
rm -f "$f" || true
|
||||
done
|
||||
|
||||
# Clear macOS provenance/quarantine metadata which can interfere with modifying files in-place.
|
||||
xattr -dr com.apple.provenance "$app_path" >/dev/null 2>&1 || true
|
||||
xattr -dr com.apple.quarantine "$app_path" >/dev/null 2>&1 || true
|
||||
|
||||
# Sign nested code first, then frameworks, then the main app bundle.
|
||||
#
|
||||
# Important: do NOT codesign individual files *inside* a .framework bundle (e.g. Sparkle.framework/Sparkle),
|
||||
# because codesign expects frameworks to be signed as bundles and may error with
|
||||
# "bundle format is ambiguous (could be app or framework)".
|
||||
|
||||
# 1) Sign dylibs and standalone executables that are NOT inside a .framework/.app/.xpc bundle.
|
||||
find "$app_path" -type f \( -name "*.dylib" -o -name "*.so" -o -perm -111 \) \
|
||||
! -name "*.cstemp" \
|
||||
! -path "*/Contents/Frameworks/*.framework/*" \
|
||||
! -path "*/Contents/Frameworks/*.app/*" \
|
||||
! -path "*/Contents/Frameworks/*.xpc/*" \
|
||||
! -path "*/Contents/PlugIns/*.framework/*" \
|
||||
! -path "*/Contents/PlugIns/*.app/*" \
|
||||
! -path "*/Contents/PlugIns/*.xpc/*" \
|
||||
-print0 | while IFS= read -r -d '' f; do
|
||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
||||
done
|
||||
|
||||
# 2) Sign nested helper apps and XPC services (Sparkle ships these inside its framework).
|
||||
find "$app_path" -type d \( -name "*.xpc" -o -name "*.app" \) -print0 2>/dev/null | while IFS= read -r -d '' b; do
|
||||
codesign "${codesign_args[@]}" "$b" >/dev/null
|
||||
done
|
||||
|
||||
# 2b) Sparkle.framework contains a standalone helper executable "Autoupdate" under Versions/* that is
|
||||
# not inside an .app or .xpc bundle. Notarization requires it be signed with Developer ID + timestamp.
|
||||
sparkle_fw="$app_path/Contents/Frameworks/Sparkle.framework"
|
||||
if [[ -d "$sparkle_fw" ]]; then
|
||||
find "$sparkle_fw/Versions" -type f -perm -111 \
|
||||
! -name "*.cstemp" \
|
||||
! -path "*/_CodeSignature/*" \
|
||||
-print0 2>/dev/null | while IFS= read -r -d '' f; do
|
||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
||||
done
|
||||
fi
|
||||
|
||||
# 3) Sign frameworks as bundles.
|
||||
find "$app_path/Contents/Frameworks" "$app_path/Contents/PlugIns" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
|
||||
codesign "${codesign_args[@]}" "$fw" >/dev/null
|
||||
done
|
||||
|
||||
# 4) Finally sign the main app.
|
||||
codesign "${codesign_args[@]}" "$app_path" >/dev/null
|
||||
|
||||
echo "==> [$(ts)] Verifying codesign"
|
||||
codesign --verify --deep --strict --verbose=2 "$app_path"
|
||||
|
||||
echo "==> [$(ts)] Creating zip for notarization"
|
||||
rm -f "$zip_path"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$app_path" "$zip_path"
|
||||
|
||||
if [[ "$skip_notarize" -eq 0 ]]; then
|
||||
# ZIP archives cannot be stapled; notarization is still useful for distribution and Sparkle.
|
||||
notarize_and_maybe_staple "$zip_path" "ZIP" 0
|
||||
echo "==> [$(ts)] Stapling app"
|
||||
xcrun stapler staple "$app_path"
|
||||
fi
|
||||
|
||||
if [[ "$do_dmg" -eq 1 ]]; then
|
||||
echo "==> [$(ts)] Building DMG (from already-signed app; no redeploy)"
|
||||
if ! command -v create-dmg >/dev/null 2>&1; then
|
||||
echo "Error: create-dmg not found. Install it with Homebrew (it's in Brewfile):" >&2
|
||||
echo " ./build_tools/macos/install_brew_deps.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build a versioned DMG name using Info.plist (falls back to Strawberry version constant).
|
||||
plist="${app_path}/Contents/Info.plist"
|
||||
bundle_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$plist" 2>/dev/null || true)"
|
||||
if [[ -z "${bundle_version}" ]]; then
|
||||
bundle_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist" 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -z "${bundle_version}" ]]; then
|
||||
bundle_version="unknown"
|
||||
fi
|
||||
arch="$(uname -m)"
|
||||
dmg_path="${build_dir}/strawberry-${bundle_version}-${arch}.dmg"
|
||||
|
||||
rm -f "$dmg_path"
|
||||
(
|
||||
cd "$build_dir"
|
||||
create-dmg \
|
||||
--volname strawberry \
|
||||
--background "${repo_root}/dist/macos/dmg_background.png" \
|
||||
--app-drop-link 450 218 \
|
||||
--icon strawberry.app 150 218 \
|
||||
--window-size 600 450 \
|
||||
"$(basename "$dmg_path")" \
|
||||
strawberry.app
|
||||
)
|
||||
if [[ -z "$dmg_path" ]]; then
|
||||
echo "Error: DMG was not created in $build_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Codesigning DMG"
|
||||
codesign --force --timestamp --sign "$identity" "$dmg_path"
|
||||
|
||||
if [[ "$skip_notarize" -eq 0 ]]; then
|
||||
notarize_and_maybe_staple "$dmg_path" "DMG" 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Gatekeeper assessment"
|
||||
spctl -a -vv --type execute "$app_path" || true
|
||||
|
||||
echo
|
||||
echo "Done."
|
||||
echo "App: $app_path"
|
||||
echo "Zip: $zip_path"
|
||||
if [[ -n "${dmg_path}" ]]; then
|
||||
echo "DMG: $dmg_path"
|
||||
fi
|
||||
|
||||
119
build_tools/macos/install_brew_deps.sh
Executable file
119
build_tools/macos/install_brew_deps.sh
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
||||
|
||||
ts() { date +"%H:%M:%S"; }
|
||||
|
||||
run_with_heartbeat() {
|
||||
local desc="$1"
|
||||
shift
|
||||
|
||||
local start now elapsed hb_pid
|
||||
start="$(date +%s)"
|
||||
|
||||
echo "==> [$(ts)] ${desc}"
|
||||
|
||||
# Heartbeat: print elapsed time periodically in case the underlying command is quiet
|
||||
(
|
||||
while true; do
|
||||
sleep 20
|
||||
now="$(date +%s)"
|
||||
elapsed="$((now - start))"
|
||||
echo " [$(ts)] ... still working (${elapsed}s elapsed) ..."
|
||||
done
|
||||
) &
|
||||
hb_pid="$!"
|
||||
|
||||
set +e
|
||||
"$@"
|
||||
local rc=$?
|
||||
set -e
|
||||
|
||||
kill "$hb_pid" >/dev/null 2>&1 || true
|
||||
wait "$hb_pid" >/dev/null 2>&1 || true
|
||||
|
||||
now="$(date +%s)"
|
||||
elapsed="$((now - start))"
|
||||
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
echo "Error: '${desc}' failed after ${elapsed}s (exit $rc)." >&2
|
||||
return "$rc"
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Done: ${desc} (${elapsed}s)"
|
||||
}
|
||||
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
echo "Error: This script is for macOS only." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
echo "Error: Homebrew ('brew') not found in PATH." >&2
|
||||
echo "Install Homebrew first: https://brew.sh/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Homebrew taps are git clones; local formula changes must be committed to be visible.
|
||||
if command -v git >/dev/null 2>&1 && git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
if git -C "$repo_root" status --porcelain Formula/ | grep -q .; then
|
||||
echo "Error: You have uncommitted changes under Formula/." >&2
|
||||
echo "Homebrew taps are git clones, so uncommitted formulae won't be visible to 'brew tap'." >&2
|
||||
echo "Commit your changes, then re-run this script." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Optional: disable auto-update for faster, more predictable runs.
|
||||
export HOMEBREW_NO_AUTO_UPDATE="${HOMEBREW_NO_AUTO_UPDATE:-1}"
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
echo "==> [$(ts)] Using repo: $repo_root"
|
||||
|
||||
# Strawberry includes local Homebrew formulae under Formula/.
|
||||
# Homebrew requires formulae to be in a tap; we tap this repo via file:// and then
|
||||
# update the tap clone to the latest commit (without untapping, since Homebrew may
|
||||
# refuse to untap when formulae from this tap are installed).
|
||||
run_with_heartbeat "Ensuring local tap exists: strawberry/local" bash -lc \
|
||||
"brew tap | grep -q '^strawberry/local$' || brew tap strawberry/local 'file://$repo_root' >/dev/null"
|
||||
|
||||
run_with_heartbeat "Refreshing strawberry/local tap clone" bash -lc '
|
||||
tap_repo="$(brew --repo strawberry/local)"
|
||||
cd "$tap_repo"
|
||||
# Make sure the remote points at the current local repo path.
|
||||
git remote set-url origin "file://'"$repo_root"'"
|
||||
git fetch -q origin
|
||||
default_ref="$(git symbolic-ref -q --short refs/remotes/origin/HEAD || true)"
|
||||
if [ -z "$default_ref" ]; then
|
||||
default_ref="origin/master"
|
||||
fi
|
||||
git reset --hard -q "$default_ref"
|
||||
'
|
||||
|
||||
for f in kdsingleapplication-qt6 qtsparkle-qt6 sparkle-framework libgpod macdeploycheck; do
|
||||
if ! brew info "strawberry/local/${f}" >/dev/null 2>&1; then
|
||||
echo "Error: Missing formula strawberry/local/${f} in the tapped repo." >&2
|
||||
echo "If you recently added/changed formulae, ensure they are committed, then refresh the tap:" >&2
|
||||
echo " git -C \"$(brew --repo strawberry/local)\" pull --ff-only" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
run_with_heartbeat "Installing dependencies from Brewfile" \
|
||||
brew bundle install --file "$repo_root/Brewfile" --verbose
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done.
|
||||
|
||||
Notes for packaging (optional):
|
||||
- The CMake target 'deploy' expects these env vars for bundling GIO + GStreamer bits:
|
||||
export GIO_EXTRA_MODULES="\$(brew --prefix)/lib/gio/modules"
|
||||
export GST_PLUGIN_SCANNER="\$(brew --prefix gstreamer)/libexec/gstreamer-1.0/gst-plugin-scanner"
|
||||
export GST_PLUGIN_PATH="\$(brew --prefix)/lib/gstreamer-1.0"
|
||||
|
||||
EOF
|
||||
|
||||
@@ -1,43 +1,104 @@
|
||||
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||
# NOTE: Packaging helpers should not be REQUIRED at configure time.
|
||||
# Missing tools should simply disable the related custom targets.
|
||||
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin)
|
||||
if(MACDEPLOYQT_EXECUTABLE)
|
||||
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing macdeployqt executable.")
|
||||
endif()
|
||||
|
||||
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin)
|
||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||
message(STATUS "Found macdeploycheck: ${MACDEPLOYCHECK_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing macdeploycheck executable.")
|
||||
message(STATUS "macdeploycheck not found (optional): 'deploycheck' target will be unavailable.")
|
||||
endif()
|
||||
|
||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg REQUIRED)
|
||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg)
|
||||
if(CREATEDMG_EXECUTABLE)
|
||||
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing create-dmg executable.")
|
||||
endif()
|
||||
|
||||
set(_SPARKLE_FRAMEWORK_DIR "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "")
|
||||
if(SPARKLE)
|
||||
# SPARKLE may be either the framework directory or the framework binary path.
|
||||
get_filename_component(_sparkle_link "${SPARKLE}" ABSOLUTE)
|
||||
get_filename_component(_sparkle_real "${SPARKLE}" REALPATH)
|
||||
if(_sparkle_link MATCHES "Sparkle\\.framework$")
|
||||
set(_SPARKLE_FRAMEWORK_DIR "${_sparkle_real}")
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "${_sparkle_link}/Versions/B/Sparkle")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "${_sparkle_real}/Versions/B/Sparkle")
|
||||
else()
|
||||
# Assume it's the framework binary path:
|
||||
# .../Sparkle.framework/Versions/B/Sparkle
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "${_sparkle_link}")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "${_sparkle_real}")
|
||||
get_filename_component(_sparkle_b_dir "${_SPARKLE_ORIGINAL_BIN_REAL}" DIRECTORY) # .../Versions/B
|
||||
get_filename_component(_sparkle_versions_dir "${_sparkle_b_dir}" DIRECTORY) # .../Versions
|
||||
get_filename_component(_SPARKLE_FRAMEWORK_DIR "${_sparkle_versions_dir}" DIRECTORY) # .../Sparkle.framework
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${_SPARKLE_FRAMEWORK_DIR}" OR NOT EXISTS "${_SPARKLE_ORIGINAL_BIN_REAL}")
|
||||
set(_SPARKLE_FRAMEWORK_DIR "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "")
|
||||
else()
|
||||
message(STATUS "Sparkle.framework found: ${_SPARKLE_FRAMEWORK_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MACDEPLOYQT_EXECUTABLE)
|
||||
|
||||
if(APPLE_DEVELOPER_ID)
|
||||
set(MACDEPLOYQT_CODESIGN -codesign=${APPLE_DEVELOPER_ID})
|
||||
set(CREATEDMG_CODESIGN --codesign ${APPLE_DEVELOPER_ID})
|
||||
endif()
|
||||
# Note: We intentionally do NOT codesign during the CMake 'deploy'/'dmg' targets.
|
||||
# macdeployqt can optionally sign, but passing identities safely through Ninja's /bin/sh wrapper is brittle.
|
||||
# This repo's signing/notarization pipeline is handled in build_tools/macos/build_sign_notarize.sh instead.
|
||||
if(CREATEDMG_SKIP_JENKINS)
|
||||
set(CREATEDMG_SKIP_JENKINS_ARG "--skip-jenkins")
|
||||
endif()
|
||||
|
||||
add_custom_target(deploy
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/{Frameworks,Resources}
|
||||
set(_deploy_commands
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Frameworks
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources
|
||||
COMMAND cp -v ${CMAKE_BINARY_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
|
||||
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS strawberry
|
||||
)
|
||||
|
||||
if(_SPARKLE_FRAMEWORK_DIR)
|
||||
list(APPEND _deploy_commands
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/bundle_sparkle.sh ${CMAKE_BINARY_DIR}/strawberry.app ${_SPARKLE_FRAMEWORK_DIR} ${_SPARKLE_ORIGINAL_BIN_LINK} ${_SPARKLE_ORIGINAL_BIN_REAL}
|
||||
)
|
||||
endif()
|
||||
|
||||
list(APPEND _deploy_commands
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner
|
||||
)
|
||||
|
||||
# Make 'deploy' incremental:
|
||||
# - add_custom_target() is always out-of-date, so it reruns every time.
|
||||
# - using a stamp file makes Ninja/Make skip deploy when inputs haven't changed.
|
||||
set(_deploy_stamp "${CMAKE_BINARY_DIR}/deploy_app_bundle.stamp")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_deploy_stamp}"
|
||||
${_deploy_commands}
|
||||
COMMAND ${CMAKE_COMMAND} -E touch "${_deploy_stamp}"
|
||||
COMMENT "Deploying app bundle (bundling Sparkle/GStreamer + macdeployqt)"
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS
|
||||
strawberry
|
||||
"${CMAKE_BINARY_DIR}/dist/macos/Info.plist"
|
||||
"${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns"
|
||||
"${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh"
|
||||
"${CMAKE_SOURCE_DIR}/dist/macos/bundle_sparkle.sh"
|
||||
)
|
||||
|
||||
add_custom_target(deploy DEPENDS "${_deploy_stamp}")
|
||||
|
||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||
add_custom_target(deploycheck
|
||||
COMMAND ${MACDEPLOYCHECK_EXECUTABLE} strawberry.app
|
||||
@@ -45,8 +106,9 @@ if(MACDEPLOYQT_EXECUTABLE)
|
||||
endif()
|
||||
if(CREATEDMG_EXECUTABLE)
|
||||
add_custom_target(dmg
|
||||
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_CODESIGN} ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
||||
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS deploy
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
31
cmake/FindRapidJSON.cmake
Normal file
31
cmake/FindRapidJSON.cmake
Normal file
@@ -0,0 +1,31 @@
|
||||
# Try to find RapidJSON (header-only).
|
||||
#
|
||||
# This project uses `find_package(RapidJSON)` and expects:
|
||||
# - RapidJSON_FOUND
|
||||
# - RapidJSON_INCLUDE_DIRS
|
||||
#
|
||||
# Homebrew's `rapidjson` formula commonly installs headers to:
|
||||
# /opt/homebrew/include/rapidjson
|
||||
# but does not always ship a `RapidJSONConfig.cmake`, so we provide this
|
||||
# Find-module fallback.
|
||||
|
||||
find_path(RapidJSON_INCLUDE_DIR
|
||||
NAMES rapidjson/document.h
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(RapidJSON
|
||||
REQUIRED_VARS RapidJSON_INCLUDE_DIR
|
||||
)
|
||||
|
||||
if(RapidJSON_FOUND)
|
||||
set(RapidJSON_INCLUDE_DIRS "${RapidJSON_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
if(RapidJSON_FOUND AND NOT TARGET RapidJSON::RapidJSON)
|
||||
add_library(RapidJSON::RapidJSON INTERFACE IMPORTED)
|
||||
set_target_properties(RapidJSON::RapidJSON PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${RapidJSON_INCLUDE_DIR}"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
set(summary_willbuild "")
|
||||
set(summary_willnotbuild "")
|
||||
|
||||
# On some platforms (notably macOS via Homebrew), many "optional" dependencies are
|
||||
# not installed by default. Historically, Strawberry treated missing optional deps
|
||||
# as a hard error when the option defaulted to ON, which makes first-time builds
|
||||
# frustrating.
|
||||
#
|
||||
# This toggle controls that behavior:
|
||||
# - ON => missing optional deps abort the configure (packager/CI-friendly)
|
||||
# - OFF => missing optional deps auto-disable the component (dev-friendly)
|
||||
set(_optional_components_fatal_default ON)
|
||||
if(APPLE)
|
||||
set(_optional_components_fatal_default OFF)
|
||||
endif()
|
||||
option(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL
|
||||
"If ON, missing optional component dependencies are fatal (otherwise components auto-disable)"
|
||||
${_optional_components_fatal_default}
|
||||
)
|
||||
|
||||
macro(optional_component_summary_add name test)
|
||||
if (${test})
|
||||
list(APPEND summary_willbuild ${name})
|
||||
@@ -80,8 +97,13 @@ function(optional_component name default description)
|
||||
set(text "${description} (missing ${deplist_text})")
|
||||
|
||||
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
|
||||
|
||||
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
||||
if(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL)
|
||||
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
||||
else()
|
||||
message(STATUS "${text} - disabling ${option_variable}")
|
||||
set(${option_variable} OFF CACHE BOOL "${description}" FORCE)
|
||||
return()
|
||||
endif()
|
||||
|
||||
else()
|
||||
set(${have_variable} ON PARENT_SCOPE)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 2)
|
||||
set(STRAWBERRY_VERSION_PATCH 11)
|
||||
set(STRAWBERRY_VERSION_PATCH 17)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
set(INCLUDE_GIT_REVISION ON)
|
||||
|
||||
set(majorminorpatch "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}.${STRAWBERRY_VERSION_PATCH}")
|
||||
|
||||
|
||||
25
cmake/qt_tool_wrapper.sh
Executable file
25
cmake/qt_tool_wrapper.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
tool="${1:-}"
|
||||
shift || true
|
||||
|
||||
if [[ -z "$tool" ]]; then
|
||||
echo "qt_tool_wrapper.sh: missing tool argument" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
base="$(basename "$tool")"
|
||||
|
||||
# Qt LinguistTools (lrelease) prints some noisy informational lines to stderr that
|
||||
# are not actionable during normal builds (e.g. "Removed plural forms...").
|
||||
# We filter only those specific messages.
|
||||
if [[ "$base" == "lrelease" ]]; then
|
||||
"$tool" "$@" 2>&1 | sed \
|
||||
-e '/^Removed plural forms as the target language has less forms\.$/d' \
|
||||
-e '/^If this sounds wrong, possibly the target language is not set or recognized\.$/d'
|
||||
exit "${PIPESTATUS[0]}"
|
||||
fi
|
||||
|
||||
exec "$tool" "$@"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<file>schema/schema-18.sql</file>
|
||||
<file>schema/schema-19.sql</file>
|
||||
<file>schema/schema-20.sql</file>
|
||||
<file>schema/schema-21.sql</file>
|
||||
<file>schema/device-schema.sql</file>
|
||||
<file>style/strawberry.css</file>
|
||||
<file>style/smartplaylistsearchterm.css</file>
|
||||
|
||||
@@ -12,9 +12,13 @@ CREATE TABLE device_%deviceid_subdirectories (
|
||||
CREATE TABLE device_%deviceid_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -22,7 +26,9 @@ CREATE TABLE device_%deviceid_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -86,7 +92,11 @@ CREATE TABLE device_%deviceid_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -94,4 +104,4 @@ CREATE INDEX idx_device_%deviceid_songs_album ON device_%deviceid_songs (album);
|
||||
|
||||
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
|
||||
|
||||
UPDATE devices SET schema_version=5 WHERE ROWID=%deviceid;
|
||||
UPDATE devices SET schema_version=6 WHERE ROWID=%deviceid;
|
||||
|
||||
43
data/schema/schema-21.sql
Normal file
43
data/schema/schema-21.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
DROP INDEX IF EXISTS idx_albumartistsort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_albumsort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_artistsort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_composersort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_performersort;
|
||||
|
||||
DROP INDEX IF EXISTS idx_titlesort;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN albumartistsort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN albumsort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN artistsort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN composersort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN performersort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN titlesort TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN bpm REAL;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN mood TEXT;
|
||||
|
||||
ALTER TABLE %allsongstables ADD COLUMN initial_key TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
|
||||
|
||||
UPDATE schema_version SET version=21;
|
||||
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
||||
|
||||
DELETE FROM schema_version;
|
||||
|
||||
INSERT INTO schema_version (version) VALUES (20);
|
||||
INSERT INTO schema_version (version) VALUES (21);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS directories (
|
||||
path TEXT NOT NULL,
|
||||
@@ -20,9 +20,13 @@ CREATE TABLE IF NOT EXISTS subdirectories (
|
||||
CREATE TABLE IF NOT EXISTS songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -30,7 +34,9 @@ CREATE TABLE IF NOT EXISTS songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -94,16 +100,24 @@ CREATE TABLE IF NOT EXISTS songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -111,7 +125,9 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -175,16 +191,24 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -192,7 +216,9 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -256,16 +282,24 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -273,7 +307,9 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -337,16 +373,24 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -354,7 +398,9 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -418,16 +464,24 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -435,7 +489,9 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -499,16 +555,24 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -516,7 +580,9 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -580,16 +646,24 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -597,7 +671,9 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -661,16 +737,24 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -678,7 +762,9 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -742,16 +828,24 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -759,7 +853,9 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -823,16 +919,24 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER NOT NULL DEFAULT -1,
|
||||
disc INTEGER NOT NULL DEFAULT -1,
|
||||
year INTEGER NOT NULL DEFAULT -1,
|
||||
@@ -840,7 +944,9 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
genre TEXT,
|
||||
compilation INTEGER NOT NULL DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -904,7 +1010,11 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -931,9 +1041,13 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
playlist_url TEXT,
|
||||
|
||||
title TEXT,
|
||||
titlesort TEXT,
|
||||
album TEXT,
|
||||
albumsort TEXT,
|
||||
artist TEXT,
|
||||
artistsort TEXT,
|
||||
albumartist TEXT,
|
||||
albumartistsort TEXT,
|
||||
track INTEGER,
|
||||
disc INTEGER,
|
||||
year INTEGER,
|
||||
@@ -941,7 +1055,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
genre TEXT,
|
||||
compilation INTEGER DEFAULT 0,
|
||||
composer TEXT,
|
||||
composersort TEXT,
|
||||
performer TEXT,
|
||||
performersort TEXT,
|
||||
grouping TEXT,
|
||||
comment TEXT,
|
||||
lyrics TEXT,
|
||||
@@ -1005,7 +1121,11 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -1032,10 +1152,22 @@ CREATE INDEX IF NOT EXISTS idx_comp_artist ON songs (compilation_effective, arti
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumartist ON songs (albumartist);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artist ON songs (artist);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
|
||||
|
||||
CREATE VIEW IF NOT EXISTS duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;
|
||||
|
||||
4
debian/control
vendored
4
debian/control
vendored
@@ -60,11 +60,11 @@ Description: music player and music collection organizer
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||
- Lyrics from multiple sources
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
- Unofficial streaming support for Tidal and Qobuz
|
||||
.
|
||||
|
||||
15
dist/CMakeLists.txt
vendored
15
dist/CMakeLists.txt
vendored
@@ -9,6 +9,21 @@ if(APPLE)
|
||||
else()
|
||||
set(LSMinimumSystemVersion 12.0)
|
||||
endif()
|
||||
|
||||
# Sparkle (macOS updates)
|
||||
# These values are embedded into Info.plist and control where the app checks for updates.
|
||||
# Downstream builders can override on the CMake command line:
|
||||
# -DSPARKLE_FEED_URL="https://example.com/appcast.xml"
|
||||
# -DSPARKLE_PUBLIC_ED25519_KEY="base64=="
|
||||
#
|
||||
# Defaults preserve upstream behavior, but are intentionally configurable for third-party builds.
|
||||
if(NOT DEFINED SPARKLE_FEED_URL)
|
||||
set(SPARKLE_FEED_URL "https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@")
|
||||
endif()
|
||||
if(NOT DEFINED SPARKLE_PUBLIC_ED25519_KEY)
|
||||
set(SPARKLE_PUBLIC_ED25519_KEY "/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=")
|
||||
endif()
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
||||
endif()
|
||||
|
||||
|
||||
4
dist/macos/Info.plist.in
vendored
4
dist/macos/Info.plist.in
vendored
@@ -35,9 +35,9 @@
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>@LSMinimumSystemVersion@</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@</string>
|
||||
<string>@SPARKLE_FEED_URL@</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=</string>
|
||||
<string>@SPARKLE_PUBLIC_ED25519_KEY@</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
85
dist/macos/bundle_sparkle.sh
vendored
Executable file
85
dist/macos/bundle_sparkle.sh
vendored
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
bundledir="${1:-}"
|
||||
sparkle_framework_dir="${2:-}"
|
||||
sparkle_bin_link="${3:-}"
|
||||
sparkle_bin_real="${4:-}"
|
||||
|
||||
if [[ -z "$bundledir" || -z "$sparkle_framework_dir" ]]; then
|
||||
echo "Usage: $0 <bundledir> <sparkle_framework_dir> [sparkle_bin_link] [sparkle_bin_real]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -d "$sparkle_framework_dir" ]]; then
|
||||
echo "Sparkle.framework dir not found: $sparkle_framework_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
src_framework_dir="$sparkle_framework_dir"
|
||||
|
||||
# Homebrew often provides /opt/homebrew/Frameworks/Sparkle.framework where Versions/* are symlinks
|
||||
# pointing back into the Cellar. Copying that verbatim breaks inside an app bundle.
|
||||
# Resolve to the real Cellar framework root via Versions/Current.
|
||||
if [[ -e "${sparkle_framework_dir}/Versions/Current" ]]; then
|
||||
current_real="$(cd "${sparkle_framework_dir}/Versions/Current" && pwd -P)"
|
||||
# current_real is .../Sparkle.framework/Versions/B (or similar)
|
||||
src_framework_dir="$(cd "${current_real}/../.." && pwd -P)"
|
||||
fi
|
||||
|
||||
dst_framework="${bundledir}/Contents/Frameworks/Sparkle.framework"
|
||||
main_bin="${bundledir}/Contents/MacOS/strawberry"
|
||||
qtsparkle_dylib="${bundledir}/Contents/Frameworks/libqtsparkle-qt6.dylib"
|
||||
|
||||
mkdir -p "${bundledir}/Contents/Frameworks"
|
||||
|
||||
echo "Bundling Sparkle.framework -> ${dst_framework}"
|
||||
rm -rf "${dst_framework}"
|
||||
# Use ditto to preserve the framework's internal symlinks/structure.
|
||||
ditto "${src_framework_dir}" "${dst_framework}"
|
||||
|
||||
# Prefer the canonical framework binary path.
|
||||
dst_bin="${dst_framework}/Versions/Current/Sparkle"
|
||||
if [[ ! -e "${dst_bin}" ]]; then
|
||||
echo "Error: Sparkle binary missing at ${dst_bin}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sparkle_rpath="@rpath/Sparkle.framework/Versions/Current/Sparkle"
|
||||
|
||||
# Sanity check: top-level Sparkle entry should be a symlink (not a copied Mach-O file).
|
||||
if [[ -e "${dst_framework}/Sparkle" && ! -L "${dst_framework}/Sparkle" ]]; then
|
||||
echo "Warning: ${dst_framework}/Sparkle is not a symlink (unexpected). This can confuse codesign." >&2
|
||||
fi
|
||||
|
||||
echo "Fixing Sparkle.framework install name"
|
||||
install_name_tool -id "${sparkle_rpath}" "${dst_bin}"
|
||||
|
||||
echo "Ensuring main binary has Frameworks rpath"
|
||||
install_name_tool -add_rpath "@executable_path/../Frameworks" "${main_bin}" || true
|
||||
|
||||
echo "Rewriting Sparkle.framework references to @rpath"
|
||||
# Try to rewrite a few common Homebrew Sparkle install names as well, because the
|
||||
# recorded install name may differ from the path returned by CMake's find_library.
|
||||
old_candidates=(
|
||||
"${sparkle_bin_link}"
|
||||
"${sparkle_bin_real}"
|
||||
"/opt/homebrew/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
||||
"/opt/homebrew/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
||||
"/opt/homebrew/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
||||
"/opt/homebrew/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
||||
"/usr/local/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
||||
"/usr/local/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
||||
"/usr/local/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
||||
"/usr/local/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
||||
)
|
||||
|
||||
for old in "${old_candidates[@]}"; do
|
||||
if [[ -n "${old}" ]]; then
|
||||
install_name_tool -change "${old}" "${sparkle_rpath}" "${main_bin}" || true
|
||||
if [[ -f "${qtsparkle_dylib}" ]]; then
|
||||
install_name_tool -change "${old}" "${sparkle_rpath}" "${qtsparkle_dylib}" || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
22
dist/macos/macversion.sh
vendored
22
dist/macos/macversion.sh
vendored
@@ -1,22 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
macos_version=$(sw_vers -productVersion)
|
||||
macos_version_major=$(echo $macos_version | awk -F '[.]' '{print $1}')
|
||||
macos_version_minor=$(echo $macos_version | awk -F '[.]' '{print $2}')
|
||||
|
||||
if [ "${macos_version_major}" = "10" ]; then
|
||||
macos_codenames=(
|
||||
["13"]="highsierra"
|
||||
["14"]="mojave"
|
||||
["15"]="catalina"
|
||||
)
|
||||
if [[ -n "${macos_codenames[$macos_version_minor]}" ]]; then
|
||||
echo "${macos_codenames[$macos_version_minor]}"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
elif [ "${macos_version_major}" = "11" ]; then
|
||||
echo "bigsur"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
@@ -31,10 +31,10 @@
|
||||
<li>Edit tags on audio files</li>
|
||||
<li>Automatically retrieve tags from MusicBrainz</li>
|
||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||
<li>Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</li>
|
||||
<li>Lyrics from multiple sources</li>
|
||||
<li>Audio analyzer and equalizer</li>
|
||||
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
||||
<li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li>
|
||||
<li>Scrobbler with support for Last.fm and ListenBrainz</li>
|
||||
<li>Streaming support for Subsonic-compatible servers</li>
|
||||
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
||||
</ul>
|
||||
@@ -51,6 +51,12 @@
|
||||
</screenshots>
|
||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||
<releases>
|
||||
<release version="1.2.17" date="2026-01-18"/>
|
||||
<release version="1.2.16" date="2025-12-16"/>
|
||||
<release version="1.2.15" date="2025-11-25"/>
|
||||
<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"/>
|
||||
|
||||
6
dist/unix/strawberry.1
vendored
6
dist/unix/strawberry.1
vendored
@@ -29,9 +29,7 @@ Features:
|
||||
.br
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
.br
|
||||
- Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
.br
|
||||
- Support for multiple backends
|
||||
- Lyrics from multiple sources
|
||||
.br
|
||||
- Audio analyzer
|
||||
.br
|
||||
@@ -39,7 +37,7 @@ Features:
|
||||
.br
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
.br
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
.br
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
.br
|
||||
|
||||
7
dist/unix/strawberry.spec.in
vendored
7
dist/unix/strawberry.spec.in
vendored
@@ -93,16 +93,15 @@ Features:
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||
- Support for multiple backends
|
||||
- Lyrics from multiple sources
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
- Unofficial streaming support for Tidal and Qobuz
|
||||
|
||||
%if 0%{?suse_version} && 0%{?suse_version} <= 1600
|
||||
%if 0%{?suse_version} && 0%{?suse_version} < 1600
|
||||
%debug_package
|
||||
%endif
|
||||
|
||||
|
||||
76
dist/windows/strawberry.nsi.in
vendored
76
dist/windows/strawberry.nsi.in
vendored
@@ -224,7 +224,7 @@ Function InstallMSVCRuntime
|
||||
; ${If} $R0 == ""
|
||||
SetDetailsView hide
|
||||
; inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
|
||||
ExecWait '"$TEMP\${vc_redist_file}" /install /passive'
|
||||
ExecWait '"$TEMP\${vc_redist_file}" /install /passive /norestart'
|
||||
Delete "$TEMP\${vc_redist_file}"
|
||||
SetDetailsView show
|
||||
; ${EndIf}
|
||||
@@ -395,9 +395,6 @@ Section "Strawberry" Strawberry
|
||||
File "glib-2.0-0.dll"
|
||||
File "gme.dll"
|
||||
File "gmodule-2.0-0.dll"
|
||||
!ifndef arch_arm64
|
||||
File "gnutls.dll"
|
||||
!endif
|
||||
File "gobject-2.0-0.dll"
|
||||
File "gstadaptivedemux-1.0-0.dll"
|
||||
File "gstapp-1.0-0.dll"
|
||||
@@ -418,16 +415,11 @@ Section "Strawberry" Strawberry
|
||||
File "gsttag-1.0-0.dll"
|
||||
File "gsturidownloader-1.0-0.dll"
|
||||
File "gstvideo-1.0-0.dll"
|
||||
File "gstwinrt-1.0-0.dll"
|
||||
File "harfbuzz.dll"
|
||||
File "intl-8.dll"
|
||||
File "jpeg62.dll"
|
||||
File "kdsingleapplication-qt6.dll"
|
||||
File "libbs2b.dll"
|
||||
!ifndef arch_arm64
|
||||
File "libfaac_dll.dll"
|
||||
!endif
|
||||
File "liblzma.dll"
|
||||
File "libmp3lame.dll"
|
||||
File "libopenmpt.dll"
|
||||
File "mpcdec.dll"
|
||||
@@ -445,6 +437,11 @@ Section "Strawberry" Strawberry
|
||||
File "vorbisfile.dll"
|
||||
File "wavpackdll.dll"
|
||||
|
||||
!ifndef arch_arm64
|
||||
File "gnutls.dll"
|
||||
File "libfaac_dll.dll"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
File "freetype.dll"
|
||||
File "libiconv.dll"
|
||||
@@ -452,10 +449,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeex.dll"
|
||||
File "pcre2-8.dll"
|
||||
File "pcre2-16.dll"
|
||||
File "zlib1.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolame.dll"
|
||||
!endif
|
||||
File "zlib1.dll"
|
||||
!endif
|
||||
!ifdef debug
|
||||
File "freetyped.dll"
|
||||
@@ -464,10 +461,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeexd.dll"
|
||||
File "pcre2-8d.dll"
|
||||
File "pcre2-16d.dll"
|
||||
File "zlibd1.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolamed.dll"
|
||||
!endif
|
||||
File "zlibd1.dll"
|
||||
!endif
|
||||
|
||||
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
|
||||
@@ -480,15 +477,15 @@ Section "Strawberry" Strawberry
|
||||
|
||||
; Common files
|
||||
|
||||
File "icudt77.dll"
|
||||
File "icudt78.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
File "fftw3.dll"
|
||||
!else
|
||||
File "libfftw3-3.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
File "icuin77d.dll"
|
||||
File "icuuc77d.dll"
|
||||
File "icuin78d.dll"
|
||||
File "icuuc78d.dll"
|
||||
File "libxml2d.dll"
|
||||
File "Qt6Concurrentd.dll"
|
||||
File "Qt6Cored.dll"
|
||||
@@ -497,8 +494,8 @@ Section "Strawberry" Strawberry
|
||||
File "Qt6Sqld.dll"
|
||||
File "Qt6Widgetsd.dll"
|
||||
!else
|
||||
File "icuin77.dll"
|
||||
File "icuuc77.dll"
|
||||
File "icuin78.dll"
|
||||
File "icuuc78.dll"
|
||||
File "libxml2.dll"
|
||||
File "Qt6Concurrent.dll"
|
||||
File "Qt6Core.dll"
|
||||
@@ -508,6 +505,7 @@ Section "Strawberry" Strawberry
|
||||
File "Qt6Widgets.dll"
|
||||
!endif
|
||||
|
||||
!ifdef msvc && arch_x86
|
||||
File "avcodec-61.dll"
|
||||
File "avfilter-10.dll"
|
||||
File "avformat-61.dll"
|
||||
@@ -515,6 +513,14 @@ Section "Strawberry" Strawberry
|
||||
File "postproc-58.dll"
|
||||
File "swresample-5.dll"
|
||||
File "swscale-8.dll"
|
||||
!else
|
||||
File "avcodec-62.dll"
|
||||
File "avfilter-11.dll"
|
||||
File "avformat-62.dll"
|
||||
File "avutil-60.dll"
|
||||
File "swresample-6.dll"
|
||||
File "swscale-9.dll"
|
||||
!endif
|
||||
|
||||
; Register Strawberry with Default Programs
|
||||
Var /GLOBAL AppIcon
|
||||
@@ -675,6 +681,7 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=libgstvolume.dll" "gstreamer-plugins\libgstvolume.dll"
|
||||
File "/oname=libgstvorbis.dll" "gstreamer-plugins\libgstvorbis.dll"
|
||||
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
|
||||
File "/oname=libgstwasapi2.dll" "gstreamer-plugins\libgstwasapi2.dll"
|
||||
File "/oname=libgstwaveform.dll" "gstreamer-plugins\libgstwaveform.dll"
|
||||
File "/oname=libgstwavenc.dll" "gstreamer-plugins\libgstwavenc.dll"
|
||||
File "/oname=libgstwavpack.dll" "gstreamer-plugins\libgstwavpack.dll"
|
||||
@@ -940,9 +947,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\glib-2.0-0.dll"
|
||||
Delete "$INSTDIR\gme.dll"
|
||||
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\gobject-2.0-0.dll"
|
||||
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstapp-1.0-0.dll"
|
||||
@@ -963,16 +967,11 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gsttag-1.0-0.dll"
|
||||
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstvideo-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstwinrt-1.0-0.dll"
|
||||
Delete "$INSTDIR\harfbuzz.dll"
|
||||
Delete "$INSTDIR\intl-8.dll"
|
||||
Delete "$INSTDIR\jpeg62.dll"
|
||||
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
|
||||
Delete "$INSTDIR\libbs2b.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\liblzma.dll"
|
||||
Delete "$INSTDIR\libmp3lame.dll"
|
||||
Delete "$INSTDIR\libopenmpt.dll"
|
||||
Delete "$INSTDIR\mpcdec.dll"
|
||||
@@ -990,6 +989,11 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\vorbisfile.dll"
|
||||
Delete "$INSTDIR\wavpackdll.dll"
|
||||
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
Delete "$INSTDIR\freetype.dll"
|
||||
Delete "$INSTDIR\libiconv.dll"
|
||||
@@ -997,10 +1001,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeex.dll"
|
||||
Delete "$INSTDIR\pcre2-8.dll"
|
||||
Delete "$INSTDIR\pcre2-16.dll"
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolame.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
!endif
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\freetyped.dll"
|
||||
@@ -1009,10 +1013,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeexd.dll"
|
||||
Delete "$INSTDIR\pcre2-8d.dll"
|
||||
Delete "$INSTDIR\pcre2-16d.dll"
|
||||
Delete "$INSTDIR\zlibd1.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolamed.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\zlibd1.dll"
|
||||
!endif
|
||||
|
||||
!ifdef arch_x86
|
||||
@@ -1024,15 +1028,15 @@ Section "Uninstall"
|
||||
|
||||
; Common files
|
||||
|
||||
Delete "$INSTDIR\icudt77.dll"
|
||||
Delete "$INSTDIR\icudt78.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
Delete "$INSTDIR\fftw3.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libfftw3-3.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
Delete "$INSTDIR\icuin77d.dll"
|
||||
Delete "$INSTDIR\icuuc77d.dll"
|
||||
Delete "$INSTDIR\icuin78d.dll"
|
||||
Delete "$INSTDIR\icuuc78d.dll"
|
||||
Delete "$INSTDIR\libxml2d.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrentd.dll"
|
||||
Delete "$INSTDIR\Qt6Cored.dll"
|
||||
@@ -1041,8 +1045,8 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\Qt6Sqld.dll"
|
||||
Delete "$INSTDIR\Qt6Widgetsd.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\icuin77.dll"
|
||||
Delete "$INSTDIR\icuuc77.dll"
|
||||
Delete "$INSTDIR\icuin78.dll"
|
||||
Delete "$INSTDIR\icuuc78.dll"
|
||||
Delete "$INSTDIR\libxml2.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrent.dll"
|
||||
Delete "$INSTDIR\Qt6Core.dll"
|
||||
@@ -1052,6 +1056,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\Qt6Widgets.dll"
|
||||
!endif
|
||||
|
||||
!ifdef msvc && arch_x86
|
||||
Delete "$INSTDIR\avcodec-61.dll"
|
||||
Delete "$INSTDIR\avfilter-10.dll"
|
||||
Delete "$INSTDIR\avformat-61.dll"
|
||||
@@ -1059,6 +1064,14 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\postproc-58.dll"
|
||||
Delete "$INSTDIR\swresample-5.dll"
|
||||
Delete "$INSTDIR\swscale-8.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\avcodec-62.dll"
|
||||
Delete "$INSTDIR\avfilter-11.dll"
|
||||
Delete "$INSTDIR\avformat-62.dll"
|
||||
Delete "$INSTDIR\avutil-60.dll"
|
||||
Delete "$INSTDIR\swresample-6.dll"
|
||||
Delete "$INSTDIR\swscale-9.dll"
|
||||
!endif
|
||||
|
||||
!ifdef mingw
|
||||
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
|
||||
@@ -1152,6 +1165,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstvolume.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstvorbis.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi2.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwaveform.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavenc.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavpack.dll"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef ANALYZERBASE_H
|
||||
#define ANALYZERBASE_H
|
||||
@@ -90,4 +90,3 @@ class AnalyzerBase : public QWidget {
|
||||
};
|
||||
|
||||
#endif // ANALYZERBASE_H
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
@@ -61,7 +61,7 @@ constexpr int kLowFramerate = 20;
|
||||
constexpr int kMediumFramerate = 25;
|
||||
constexpr int kHighFramerate = 30;
|
||||
constexpr int kSuperHighFramerate = 60;
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
AnalyzerContainer::AnalyzerContainer(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef ANALYZERCONTAINER_H
|
||||
#define ANALYZERCONTAINER_H
|
||||
@@ -101,8 +101,7 @@ void AnalyzerContainer::AddAnalyzerType() {
|
||||
group_->addAction(action);
|
||||
action->setCheckable(true);
|
||||
actions_ << action;
|
||||
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); } );
|
||||
|
||||
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); });
|
||||
}
|
||||
|
||||
#endif // ANALYZERCONTAINER_H
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "blockanalyzer.h"
|
||||
|
||||
@@ -61,11 +61,12 @@ BlockAnalyzer::BlockAnalyzer(QWidget *parent)
|
||||
fade_intensity_(1 << 8, 32),
|
||||
step_(0) {
|
||||
|
||||
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1); //-1 is padding, no drawing takes place there
|
||||
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1); // -1 is padding, no drawing takes place there
|
||||
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
|
||||
|
||||
// mxcl says null pixmaps cause crashes, so let's play it safe
|
||||
std::fill(fade_bars_.begin(), fade_bars_.end(), QPixmap(1, 1));
|
||||
|
||||
}
|
||||
|
||||
void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
|
||||
@@ -237,7 +238,7 @@ static inline void adjustToLimits(const int b, int &f, int &amount) {
|
||||
* Clever contrast function
|
||||
*
|
||||
* It will try to adjust the foreground color such that it contrasts well with
|
||||
*the background
|
||||
* the background
|
||||
* It won't modify the hue of fg unless absolutely necessary
|
||||
* @return the adjusted form of fg
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef BLOCKANALYZER_H
|
||||
#define BLOCKANALYZER_H
|
||||
@@ -41,14 +41,14 @@ class BlockAnalyzer : public AnalyzerBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE explicit BlockAnalyzer(QWidget*);
|
||||
Q_INVOKABLE explicit BlockAnalyzer(QWidget *parent);
|
||||
|
||||
static const char *kName;
|
||||
|
||||
protected:
|
||||
void transform(Scope&) override;
|
||||
void transform(Scope &s) override;
|
||||
void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
|
||||
void resizeEvent(QResizeEvent*) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
virtual void paletteChange(const QPalette &_palette);
|
||||
void framerateChanged() override;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "boomanalyzer.h"
|
||||
|
||||
@@ -143,7 +143,7 @@ void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame
|
||||
bar_height_[i] = std::max(0.0, bar_height_[i]);
|
||||
}
|
||||
|
||||
peak_handling:
|
||||
peak_handling:
|
||||
|
||||
if (peak_height_[i] > 0.0) {
|
||||
peak_height_[i] -= peak_speed_[i];
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef BOOMANALYZER_H
|
||||
#define BOOMANALYZER_H
|
||||
@@ -40,7 +40,7 @@ class BoomAnalyzer : public AnalyzerBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE explicit BoomAnalyzer(QWidget*);
|
||||
Q_INVOKABLE explicit BoomAnalyzer(QWidget *parent);
|
||||
|
||||
static const char *kName;
|
||||
|
||||
@@ -70,7 +70,6 @@ class BoomAnalyzer : public AnalyzerBase {
|
||||
|
||||
QPixmap barPixmap_;
|
||||
QPixmap canvas_;
|
||||
|
||||
};
|
||||
|
||||
#endif // BOOMANALYZER_H
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "fht.h"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef FHT_H
|
||||
#define FHT_H
|
||||
@@ -55,20 +55,20 @@ class FHT {
|
||||
/**
|
||||
* Recursive in-place Hartley transform. For internal use only!
|
||||
*/
|
||||
void _transform(float*, int, int);
|
||||
void _transform(float *p, int n, int k);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Prepare transform for data sets with @f$2^n@f$ numbers, whereby @f$n@f$
|
||||
* should be at least 3. Values of more than 3 need a trigonometry table.
|
||||
* @see makeCasTable()
|
||||
*/
|
||||
* Prepare transform for data sets with @f$2^n@f$ numbers, whereby @f$n@f$
|
||||
* should be at least 3. Values of more than 3 need a trigonometry table.
|
||||
* @see makeCasTable()
|
||||
*/
|
||||
explicit FHT(uint);
|
||||
|
||||
~FHT();
|
||||
int sizeExp() const;
|
||||
int size() const;
|
||||
void scale(float*, float) const;
|
||||
void scale(float *p, float d) const;
|
||||
|
||||
/**
|
||||
* Exponentially Weighted Moving Average (EWMA) filter.
|
||||
@@ -90,12 +90,12 @@ class FHT {
|
||||
/**
|
||||
* Semi-logarithmic audio spectrum.
|
||||
*/
|
||||
void semiLogSpectrum(float*);
|
||||
void semiLogSpectrum(float *p);
|
||||
|
||||
/**
|
||||
* Fourier spectrum.
|
||||
*/
|
||||
void spectrum(float*);
|
||||
void spectrum(float *p);
|
||||
|
||||
/**
|
||||
* Calculates a mathematically correct FFT power spectrum.
|
||||
@@ -103,7 +103,7 @@ class FHT {
|
||||
* and factor the 0.5 in the final scaling factor.
|
||||
* @see FHT::power2()
|
||||
*/
|
||||
void power(float*);
|
||||
void power(float *p);
|
||||
|
||||
/**
|
||||
* Calculates an FFT power spectrum with doubled values as a
|
||||
@@ -112,14 +112,14 @@ class FHT {
|
||||
* of @f$2^n@f$ input values. This is the fastest transform.
|
||||
* @see FHT::power()
|
||||
*/
|
||||
void power2(float*);
|
||||
void power2(float *p);
|
||||
|
||||
/**
|
||||
* Discrete Hartley transform of data sets with 8 values.
|
||||
*/
|
||||
static void transform8(float*);
|
||||
static void transform8(float *p);
|
||||
|
||||
void transform(float*);
|
||||
void transform(float *p);
|
||||
};
|
||||
|
||||
#endif // FHT_H
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "rainbowanalyzer.h"
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef RAINBOWANALYZER_H
|
||||
#define RAINBOWANALYZER_H
|
||||
@@ -85,7 +85,7 @@ class RainbowAnalyzer : public AnalyzerBase {
|
||||
|
||||
private:
|
||||
// "constants" that get initialized in the constructor
|
||||
float band_scale_[kRainbowBands] {};
|
||||
float band_scale_[kRainbowBands]{};
|
||||
QPen colors_[kRainbowBands];
|
||||
|
||||
// Rainbow Nyancat & Dash
|
||||
@@ -96,7 +96,7 @@ class RainbowAnalyzer : public AnalyzerBase {
|
||||
int frame_;
|
||||
|
||||
// The y positions of each point on the rainbow.
|
||||
float history_[kHistorySize * kRainbowBands] {};
|
||||
float history_[kHistorySize * kRainbowBands]{};
|
||||
|
||||
// A cache of the last frame's rainbow,
|
||||
// so it can be used in the next frame.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef SONOGRAMANALYZER_H
|
||||
#define SONOGRAMANALYZER_H
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
@@ -70,7 +70,7 @@ void TurbineAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_fr
|
||||
bar_height_[i] = std::max(0.0, bar_height_[i]);
|
||||
}
|
||||
|
||||
peak_handling:
|
||||
peak_handling:
|
||||
if (peak_height_[i] > 0.0) {
|
||||
peak_height_[i] -= peak_speed_[i];
|
||||
peak_speed_[i] *= F_peakSpeed_; // 1.12
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef TURBINEANALYZER_H
|
||||
#define TURBINEANALYZER_H
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
|
||||
#include <QPixmap>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -537,10 +537,24 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
if (subdir.mtime == 0) {
|
||||
// Delete the subdirectory
|
||||
// See if this subdirectory already exists in the database
|
||||
bool exists = false;
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
exists = q.next();
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
@@ -549,42 +563,36 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
||||
}
|
||||
}
|
||||
else {
|
||||
// See if this subdirectory already exists in the database
|
||||
bool exists = false;
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
exists = q.next();
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
transaction.Commit();
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::DeleteSubdirs(const CollectionSubdirectoryList &subdirs) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -140,7 +140,6 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
Q_INVOKABLE explicit CollectionBackend(QObject *parent = nullptr);
|
||||
|
||||
~CollectionBackend();
|
||||
@@ -253,6 +252,7 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
void DeleteSongsByUrls(const QList<QUrl> &url);
|
||||
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
|
||||
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||
void DeleteSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||
void CompilationsNeedUpdating();
|
||||
void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded);
|
||||
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual);
|
||||
@@ -331,4 +331,3 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
};
|
||||
|
||||
#endif // COLLECTIONBACKEND_H
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
class CollectionFilterOptions {
|
||||
public:
|
||||
|
||||
explicit CollectionFilterOptions();
|
||||
|
||||
// Filter mode:
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QRegularExpression>
|
||||
#include <QInputDialog>
|
||||
#include <QList>
|
||||
@@ -295,19 +296,21 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
|
||||
if (version == 1) {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(name).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
ds >> g;
|
||||
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
|
||||
ret->addAction(CreateGroupByAction(QUrl::fromPercentEncoding(name.toUtf8()), parent, g));
|
||||
}
|
||||
}
|
||||
else {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
s.remove(saved.at(i));
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
s.remove(name);
|
||||
}
|
||||
}
|
||||
s.endGroup();
|
||||
@@ -339,7 +342,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
||||
|
||||
if (!model_) return;
|
||||
|
||||
QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
const QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
if (name.isEmpty()) return;
|
||||
|
||||
qLog(Debug) << "Saving current grouping to" << name;
|
||||
@@ -355,7 +358,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
||||
QDataStream datastream(&buffer, QIODevice::WriteOnly);
|
||||
datastream << model_->GetGroupBy();
|
||||
s.setValue("version", u"1"_s);
|
||||
s.setValue(name, buffer);
|
||||
s.setValue(QUrl::toPercentEncoding(name), buffer);
|
||||
s.endGroup();
|
||||
|
||||
UpdateGroupByActions();
|
||||
|
||||
@@ -135,4 +135,3 @@ class CollectionFilterWidget : public QWidget {
|
||||
};
|
||||
|
||||
#endif // COLLECTIONFILTERWIDGET_H
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -78,6 +78,8 @@ CollectionLibrary::CollectionLibrary(const SharedPtr<Database> database,
|
||||
|
||||
model_ = new CollectionModel(backend_, albumcover_loader, this);
|
||||
|
||||
full_rescan_revisions_[21] = tr("Support for sort tags artist, album, album artist, title, composer, and performer");
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
@@ -122,6 +124,7 @@ void CollectionLibrary::Init() {
|
||||
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
|
||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsDeleted, &*backend_, &CollectionBackend::DeleteSubdirs);
|
||||
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
|
||||
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);
|
||||
|
||||
@@ -187,6 +190,26 @@ void CollectionLibrary::ReloadSettings() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::CurrentSongChanged(const Song &song) {
|
||||
|
||||
current_song_url_ = song.url();
|
||||
|
||||
if (!pending_song_saves_.isEmpty()) {
|
||||
SavePendingPlaycountsAndRatings();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::Stopped() {
|
||||
|
||||
current_song_url_ = QUrl();
|
||||
|
||||
if (!pending_song_saves_.isEmpty()) {
|
||||
SavePendingPlaycountsAndRatings();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SyncPlaycountAndRatingToFilesAsync() {
|
||||
|
||||
(void)QtConcurrent::run(&CollectionLibrary::SyncPlaycountAndRatingToFiles, this);
|
||||
@@ -210,18 +233,85 @@ void CollectionLibrary::SyncPlaycountAndRatingToFiles() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SongsPlaycountChanged(const SongList &songs, const bool save_tags) const {
|
||||
void CollectionLibrary::SongsPlaycountChanged(const SongList &songs, const bool save_tags) {
|
||||
|
||||
if (save_tags || save_playcounts_to_files_) {
|
||||
tagreader_client_->SaveSongsPlaycountAsync(songs);
|
||||
SongList songs_to_save_now;
|
||||
for (const Song &song : songs) {
|
||||
if (song.url().isLocalFile() && song.url() == current_song_url_ &&
|
||||
(song.filetype() == Song::FileType::OggFlac || song.filetype() == Song::FileType::OggVorbis || song.filetype() == Song::FileType::OggOpus)) {
|
||||
qLog(Debug) << "Deferring playcount save for currently playing file" << song.url().toLocalFile();
|
||||
if (pending_song_saves_.contains(song.url())) {
|
||||
SharedPtr<PendingSongSave> pending_song_save = pending_song_saves_[song.url()];
|
||||
pending_song_save->save_playcount = true;
|
||||
pending_song_save->song.set_playcount(song.playcount());
|
||||
}
|
||||
else {
|
||||
SharedPtr<PendingSongSave> pending_song_save = make_shared<PendingSongSave>();
|
||||
pending_song_save->save_playcount = true;
|
||||
pending_song_save->song = song;
|
||||
pending_song_saves_.insert(song.url(), pending_song_save);
|
||||
}
|
||||
}
|
||||
else {
|
||||
songs_to_save_now << song;
|
||||
}
|
||||
}
|
||||
if (!songs_to_save_now.isEmpty()) {
|
||||
tagreader_client_->SaveSongsPlaycountAsync(songs_to_save_now);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SongsRatingChanged(const SongList &songs, const bool save_tags) const {
|
||||
void CollectionLibrary::SongsRatingChanged(const SongList &songs, const bool save_tags) {
|
||||
|
||||
if (save_tags || save_ratings_to_files_) {
|
||||
tagreader_client_->SaveSongsRatingAsync(songs);
|
||||
SongList songs_to_save_now;
|
||||
for (const Song &song : songs) {
|
||||
if (song.url().isLocalFile() && song.url() == current_song_url_ &&
|
||||
(song.filetype() == Song::FileType::OggFlac || song.filetype() == Song::FileType::OggVorbis || song.filetype() == Song::FileType::OggOpus)) {
|
||||
qLog(Debug) << "Deferring rating save for currently playing file" << song.url().toLocalFile();
|
||||
if (pending_song_saves_.contains(song.url())) {
|
||||
SharedPtr<PendingSongSave> pending_song_save = pending_song_saves_[song.url()];
|
||||
pending_song_save->save_rating = true;
|
||||
pending_song_save->song.set_rating(song.rating());
|
||||
}
|
||||
else {
|
||||
SharedPtr<PendingSongSave> pending_song_save = make_shared<PendingSongSave>();
|
||||
pending_song_save->save_rating = true;
|
||||
pending_song_save->song = song;
|
||||
pending_song_saves_.insert(song.url(), pending_song_save);
|
||||
}
|
||||
}
|
||||
else {
|
||||
songs_to_save_now << song;
|
||||
}
|
||||
}
|
||||
if (!songs_to_save_now.isEmpty()) {
|
||||
tagreader_client_->SaveSongsRatingAsync(songs_to_save_now);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SavePendingPlaycountsAndRatings() {
|
||||
|
||||
for (auto it = pending_song_saves_.constBegin(); it != pending_song_saves_.constEnd();) {
|
||||
const QUrl url = it.key();
|
||||
SharedPtr<PendingSongSave> pending_song_save = it.value();
|
||||
if (url == current_song_url_) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
qLog(Debug) << "Saving deferred playcount/rating for" << url.toLocalFile();
|
||||
if (pending_song_save->save_playcount) {
|
||||
tagreader_client_->SaveSongsPlaycountAsync(SongList() << pending_song_save->song);
|
||||
}
|
||||
if (pending_song_save->save_rating) {
|
||||
tagreader_client_->SaveSongsRatingAsync(SongList() << pending_song_save->song);
|
||||
}
|
||||
it = pending_song_saves_.erase(it);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
@@ -71,6 +72,7 @@ class CollectionLibrary : public QObject {
|
||||
|
||||
private:
|
||||
void SyncPlaycountAndRatingToFiles();
|
||||
void SavePendingPlaycountsAndRatings();
|
||||
|
||||
public Q_SLOTS:
|
||||
void ReloadSettings();
|
||||
@@ -84,16 +86,26 @@ class CollectionLibrary : public QObject {
|
||||
|
||||
void IncrementalScan();
|
||||
|
||||
void CurrentSongChanged(const Song &song);
|
||||
void Stopped();
|
||||
|
||||
private Q_SLOTS:
|
||||
void ExitReceived();
|
||||
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false) const;
|
||||
void SongsRatingChanged(const SongList &songs, const bool save_tags = false) const;
|
||||
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false);
|
||||
void SongsRatingChanged(const SongList &songs, const bool save_tags = false);
|
||||
|
||||
Q_SIGNALS:
|
||||
void Error(const QString &error);
|
||||
void ExitFinished();
|
||||
|
||||
private:
|
||||
class PendingSongSave {
|
||||
public:
|
||||
Song song;
|
||||
bool save_playcount = false;
|
||||
bool save_rating = false;
|
||||
};
|
||||
|
||||
const SharedPtr<TaskManager> task_manager_;
|
||||
const SharedPtr<TagReaderClient> tagreader_client_;
|
||||
|
||||
@@ -111,6 +123,10 @@ class CollectionLibrary : public QObject {
|
||||
|
||||
bool save_playcounts_to_files_;
|
||||
bool save_ratings_to_files_;
|
||||
|
||||
QUrl current_song_url_;
|
||||
|
||||
QMap<QUrl, SharedPtr<PendingSongSave>> pending_song_saves_;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -54,6 +54,7 @@
|
||||
|
||||
#include "includes/scoped_ptr.h"
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "constants/collectionsettings.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/standardpaths.h"
|
||||
#include "core/database.h"
|
||||
@@ -71,12 +72,12 @@
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
#include "covermanager/albumcoverloader.h"
|
||||
#include "constants/collectionsettings.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
const int CollectionModel::kPrettyCoverSize = 32;
|
||||
|
||||
namespace {
|
||||
constexpr char kPixmapDiskCacheDir[] = "pixmapcache";
|
||||
constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists");
|
||||
@@ -88,7 +89,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
|
||||
albumcover_loader_(albumcover_loader),
|
||||
dir_model_(new CollectionDirectoryModel(backend, this)),
|
||||
filter_(new CollectionFilter(this)),
|
||||
timer_reload_(new QTimer(this)),
|
||||
timer_update_(new QTimer(this)),
|
||||
icon_artist_(IconLoader::Load(u"folder-sound"_s)),
|
||||
use_disk_cache_(false),
|
||||
@@ -130,10 +130,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
|
||||
backend_->UpdateTotalArtistCountAsync();
|
||||
backend_->UpdateTotalAlbumCountAsync();
|
||||
|
||||
timer_reload_->setSingleShot(true);
|
||||
timer_reload_->setInterval(300ms);
|
||||
QObject::connect(timer_reload_, &QTimer::timeout, this, &CollectionModel::Reload);
|
||||
|
||||
timer_update_->setSingleShot(false);
|
||||
timer_update_->setInterval(20ms);
|
||||
QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate);
|
||||
@@ -191,13 +187,9 @@ void CollectionModel::EndReset() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::Reload() {
|
||||
void CollectionModel::ResetInternal() {
|
||||
|
||||
loading_ = true;
|
||||
if (timer_reload_->isActive()) {
|
||||
timer_reload_->stop();
|
||||
}
|
||||
updates_.clear();
|
||||
|
||||
options_active_ = options_current_;
|
||||
|
||||
@@ -211,22 +203,16 @@ void CollectionModel::Reload() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ScheduleReset() {
|
||||
|
||||
if (!timer_reload_->isActive()) {
|
||||
timer_reload_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ReloadSettings() {
|
||||
|
||||
Settings settings;
|
||||
settings.beginGroup(CollectionSettings::kSettingsGroup);
|
||||
const bool show_pretty_covers = settings.value(CollectionSettings::kPrettyCovers, true).toBool();
|
||||
const bool show_dividers= settings.value(CollectionSettings::kShowDividers, true).toBool();
|
||||
const bool show_dividers = settings.value(CollectionSettings::kShowDividers, true).toBool();
|
||||
const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool();
|
||||
const bool sort_skips_articles = settings.value(CollectionSettings::kSortSkipsArticles, true).toBool();
|
||||
const bool sort_skip_articles_for_artists = settings.value(CollectionSettings::kSkipArticlesForArtists, true).toBool();
|
||||
const bool sort_skip_articles_for_albums = settings.value(CollectionSettings::kSkipArticlesForAlbums, false).toBool();
|
||||
const bool use_sort_tags = settings.value(CollectionSettings::kUseSortTags, true).toBool();
|
||||
|
||||
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
|
||||
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
|
||||
@@ -241,11 +227,15 @@ void CollectionModel::ReloadSettings() {
|
||||
if (show_pretty_covers != options_current_.show_pretty_covers ||
|
||||
show_dividers != options_current_.show_dividers ||
|
||||
show_various_artists != options_current_.show_various_artists ||
|
||||
sort_skips_articles != options_current_.sort_skips_articles) {
|
||||
sort_skip_articles_for_artists != options_current_.sort_skip_articles_for_artists ||
|
||||
sort_skip_articles_for_albums != options_current_.sort_skip_articles_for_albums ||
|
||||
use_sort_tags != options_current_.use_sort_tags) {
|
||||
options_current_.show_pretty_covers = show_pretty_covers;
|
||||
options_current_.show_dividers = show_dividers;
|
||||
options_current_.show_various_artists = show_various_artists;
|
||||
options_current_.sort_skips_articles = sort_skips_articles;
|
||||
options_current_.sort_skip_articles_for_artists = sort_skip_articles_for_artists;
|
||||
options_current_.sort_skip_articles_for_albums = sort_skip_articles_for_albums;
|
||||
options_current_.use_sort_tags = use_sort_tags;
|
||||
ScheduleReset();
|
||||
}
|
||||
|
||||
@@ -421,10 +411,15 @@ void CollectionModel::RemoveSongs(const SongList &songs) {
|
||||
|
||||
void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) {
|
||||
|
||||
for (qint64 i = 0; i < songs.count(); i += 400LL) {
|
||||
const qint64 number = std::min(songs.count() - i, 400LL);
|
||||
const SongList songs_to_queue = songs.mid(i, number);
|
||||
updates_.enqueue(CollectionModelUpdate(type, songs_to_queue));
|
||||
if (type == CollectionModelUpdate::Type::Reset) {
|
||||
updates_.enqueue(CollectionModelUpdate(type));
|
||||
}
|
||||
else {
|
||||
for (qint64 i = 0; i < songs.count(); i += 400LL) {
|
||||
const qint64 number = std::min(songs.count() - i, 400LL);
|
||||
const SongList songs_to_queue = songs.mid(i, number);
|
||||
updates_.enqueue(CollectionModelUpdate(type, songs_to_queue));
|
||||
}
|
||||
}
|
||||
|
||||
if (!timer_update_->isActive()) {
|
||||
@@ -433,6 +428,14 @@ void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, con
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ScheduleReset() {
|
||||
|
||||
if (!updates_.isEmpty() && updates_.first().type == CollectionModelUpdate::Type::Reset) return;
|
||||
|
||||
ScheduleUpdate(CollectionModelUpdate::Type::Reset);
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ScheduleAddSongs(const SongList &songs) {
|
||||
|
||||
ScheduleUpdate(CollectionModelUpdate::Type::Add, songs);
|
||||
@@ -465,6 +468,9 @@ void CollectionModel::ProcessUpdate() {
|
||||
}
|
||||
|
||||
switch (update.type) {
|
||||
case CollectionModelUpdate::Type::Reset:
|
||||
ResetInternal();
|
||||
break;
|
||||
case CollectionModelUpdate::Type::AddReAddOrUpdate:
|
||||
AddReAddOrUpdateSongsInternal(update.songs);
|
||||
break;
|
||||
@@ -539,7 +545,10 @@ void CollectionModel::AddSongsInternal(const SongList &songs) {
|
||||
// Sanity check to make sure we don't add songs that are outside the user's filter
|
||||
if (!options_active_.filter_options.Matches(song)) continue;
|
||||
|
||||
if (song_nodes_.contains(song.id())) continue;
|
||||
if (song_nodes_.contains(song.id())) {
|
||||
qLog(Debug) << song.id() << song.title() << "already exists, skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Before we can add each song we need to make sure the required container items already exist in the tree.
|
||||
// These depend on which "group by" settings the user has on the collection.
|
||||
@@ -680,8 +689,8 @@ void CollectionModel::RemoveSongsInternal(const SongList &songs) {
|
||||
if (!divider_nodes_.contains(divider_key)) continue;
|
||||
|
||||
// Look to see if there are any other items still under this divider
|
||||
QList<CollectionItem*> container_nodes = container_nodes_[0].values();
|
||||
if (std::any_of(container_nodes.begin(), container_nodes.end(), [this, divider_key](CollectionItem *node){ return DividerKey(options_active_.group_by[0], node->metadata, node->sort_text) == divider_key; })) {
|
||||
QList<CollectionItem *> container_nodes = container_nodes_[0].values();
|
||||
if (std::any_of(container_nodes.begin(), container_nodes.end(), [this, divider_key](CollectionItem *node) { return DividerKey(options_active_.group_by[0], node->metadata, node->sort_text) == divider_key; })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -699,7 +708,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
||||
|
||||
QString divider_key;
|
||||
if (options_active_.show_dividers && container_level == 0) {
|
||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skips_articles));
|
||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums, options_active_.use_sort_tags));
|
||||
if (!divider_key.isEmpty()) {
|
||||
if (!divider_nodes_.contains(divider_key)) {
|
||||
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
|
||||
@@ -713,7 +722,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
||||
item->container_level = container_level;
|
||||
item->container_key = container_key;
|
||||
item->display_text = DisplayText(group_by, song);
|
||||
item->sort_text = SortText(group_by, song, options_active_.sort_skips_articles);
|
||||
item->sort_text = SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums, options_active_.use_sort_tags);
|
||||
if (!divider_key.isEmpty()) {
|
||||
item->sort_text.prepend(divider_key + QLatin1Char(' '));
|
||||
}
|
||||
@@ -1068,39 +1077,39 @@ 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, const bool use_sort_tags) {
|
||||
|
||||
switch (group_by) {
|
||||
case GroupBy::AlbumArtist:
|
||||
return SortTextForArtist(song.effective_albumartist(), sort_skips_articles);
|
||||
return SortTextForName(use_sort_tags ? song.effective_albumartistsort() : song.effective_albumartist(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Artist:
|
||||
return SortTextForArtist(song.artist(), sort_skips_articles);
|
||||
return SortTextForName(use_sort_tags ? song.effective_artistsort() : song.artist(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Album:
|
||||
return SortText(song.album());
|
||||
return SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums);
|
||||
case GroupBy::AlbumDisc:
|
||||
return song.album() + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::YearAlbum:
|
||||
return SortTextForNumber(std::max(0, song.year())) + song.grouping() + song.album();
|
||||
return SortTextForYear(song.year()) + song.grouping() + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums);
|
||||
case GroupBy::YearAlbumDisc:
|
||||
return SortTextForNumber(std::max(0, song.year())) + song.album() + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForYear(song.year()) + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::OriginalYearAlbum:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.grouping() + song.album();
|
||||
return SortTextForYear(song.effective_originalyear()) + song.grouping() + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums);
|
||||
case GroupBy::OriginalYearAlbumDisc:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.album() + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForYear(song.effective_originalyear()) + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::Disc:
|
||||
return SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::Year:
|
||||
return SortTextForNumber(std::max(0, song.year())) + QLatin1Char(' ');
|
||||
return SortTextForYear(song.year()) + QLatin1Char(' ');
|
||||
case GroupBy::OriginalYear:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
|
||||
return SortTextForYear(song.effective_originalyear()) + QLatin1Char(' ');
|
||||
case GroupBy::Genre:
|
||||
return SortTextForArtist(song.genre(), sort_skips_articles);
|
||||
return SortText(song.genre());
|
||||
case GroupBy::Composer:
|
||||
return SortTextForArtist(song.composer(), sort_skips_articles);
|
||||
return SortTextForName(use_sort_tags ? song.effective_composersort() : song.composer(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Performer:
|
||||
return SortTextForArtist(song.performer(), sort_skips_articles);
|
||||
return SortTextForName(use_sort_tags ? song.effective_performersort() : song.performer(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Grouping:
|
||||
return SortTextForArtist(song.grouping(), sort_skips_articles);
|
||||
return SortText(song.grouping());
|
||||
case GroupBy::FileType:
|
||||
return song.TextForFiletype();
|
||||
case GroupBy::Format:
|
||||
@@ -1135,21 +1144,9 @@ QString CollectionModel::SortText(QString text) {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortTextForArtist(QString artist, const bool skip_articles) {
|
||||
QString CollectionModel::SortTextForName(const QString &name, const bool sort_skip_articles) {
|
||||
|
||||
artist = SortText(artist);
|
||||
|
||||
if (skip_articles) {
|
||||
for (const auto &i : Song::kArticles) {
|
||||
if (artist.startsWith(i)) {
|
||||
qint64 ilen = i.length();
|
||||
artist = artist.right(artist.length() - ilen) + ", "_L1 + i.left(ilen - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return artist;
|
||||
return sort_skip_articles ? SkipArticles(SortText(name)) : SortText(name);
|
||||
|
||||
}
|
||||
|
||||
@@ -1168,18 +1165,32 @@ QString CollectionModel::SortTextForSong(const Song &song) {
|
||||
|
||||
QString CollectionModel::SortTextForYear(const int year) {
|
||||
|
||||
QString str = QString::number(year);
|
||||
const QString str = QString::number(std::max(year, 0));
|
||||
return QStringLiteral("0").repeated(qMax(0, 4 - str.length())) + str;
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortTextForBitrate(const int bitrate) {
|
||||
|
||||
QString str = QString::number(bitrate);
|
||||
const QString str = QString::number(bitrate);
|
||||
return QStringLiteral("0").repeated(qMax(0, 3 - str.length())) + str;
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SkipArticles(QString name) {
|
||||
|
||||
for (const auto &i : Song::kArticles) {
|
||||
if (name.startsWith(i)) {
|
||||
qint64 ilen = i.length();
|
||||
name = name.right(name.length() - ilen) + ", "_L1 + i.left(ilen - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
|
||||
}
|
||||
|
||||
bool CollectionModel::IsSongTitleDataChanged(const Song &song1, const Song &song2) {
|
||||
|
||||
return song1.url() != song2.url() ||
|
||||
@@ -1210,27 +1221,30 @@ QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song,
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
case GroupBy::AlbumDisc:
|
||||
key = PrettyAlbumDisc(song.album(), song.disc());
|
||||
key = TextOrUnknown(song.album());
|
||||
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
|
||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
case GroupBy::YearAlbum:
|
||||
key = PrettyYearAlbum(song.year(), song.album());
|
||||
key = SortTextForYear(song.year()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
case GroupBy::YearAlbumDisc:
|
||||
key = PrettyYearAlbumDisc(song.year(), song.album(), song.disc());
|
||||
key = SortTextForYear(song.year()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
|
||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
case GroupBy::OriginalYearAlbum:
|
||||
key = PrettyYearAlbum(song.effective_originalyear(), song.album());
|
||||
key = SortTextForYear(song.effective_originalyear()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
case GroupBy::OriginalYearAlbumDisc:
|
||||
key = PrettyYearAlbumDisc(song.effective_originalyear(), song.album(), song.disc());
|
||||
key = SortTextForYear(song.effective_originalyear()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
|
||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
@@ -1238,10 +1252,10 @@ QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song,
|
||||
key = PrettyDisc(song.disc());
|
||||
break;
|
||||
case GroupBy::Year:
|
||||
key = QString::number(std::max(0, song.year()));
|
||||
key = SortTextForYear(song.year());
|
||||
break;
|
||||
case GroupBy::OriginalYear:
|
||||
key = QString::number(std::max(0, song.effective_originalyear()));
|
||||
key = SortTextForYear(song.effective_originalyear());
|
||||
break;
|
||||
case GroupBy::Genre:
|
||||
key = TextOrUnknown(song.genre());
|
||||
@@ -1333,7 +1347,7 @@ QString CollectionModel::DividerKey(const GroupBy group_by, const Song &song, co
|
||||
case GroupBy::Bitdepth:
|
||||
return SortTextForNumber(song.bitdepth());
|
||||
case GroupBy::Bitrate:
|
||||
return SortTextForNumber(song.bitrate());
|
||||
return SortTextForBitrate(song.bitrate());
|
||||
case GroupBy::None:
|
||||
case GroupBy::GroupByCount:
|
||||
return QString();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -129,14 +129,18 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
show_dividers(true),
|
||||
show_pretty_covers(true),
|
||||
show_various_artists(true),
|
||||
sort_skips_articles(true),
|
||||
sort_skip_articles_for_artists(false),
|
||||
sort_skip_articles_for_albums(false),
|
||||
use_sort_tags(true),
|
||||
separate_albums_by_grouping(false) {}
|
||||
|
||||
Grouping group_by;
|
||||
bool show_dividers;
|
||||
bool show_pretty_covers;
|
||||
bool show_various_artists;
|
||||
bool sort_skips_articles;
|
||||
bool sort_skip_articles_for_artists;
|
||||
bool sort_skip_articles_for_albums;
|
||||
bool use_sort_tags;
|
||||
bool separate_albums_by_grouping;
|
||||
CollectionFilterOptions filter_options;
|
||||
};
|
||||
@@ -183,13 +187,14 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
||||
static QString PrettyDisc(const int disc);
|
||||
static QString PrettyFormat(const Song &song);
|
||||
QString SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles);
|
||||
static QString SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums, const bool use_sort_tags);
|
||||
static QString SortText(QString text);
|
||||
static QString SortTextForName(const QString &name, const bool sort_skip_articles);
|
||||
static QString SortTextForNumber(const int number);
|
||||
static QString SortTextForArtist(QString artist, const bool skip_articles);
|
||||
static QString SortTextForSong(const Song &song);
|
||||
static QString SortTextForYear(const int year);
|
||||
static QString SortTextForBitrate(const int bitrate);
|
||||
static QString SkipArticles(QString name);
|
||||
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
|
||||
QString ContainerKey(const GroupBy group_by, const Song &song, bool &has_unique_album_identifier) const;
|
||||
|
||||
@@ -228,7 +233,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
|
||||
QVariant data(CollectionItem *item, const int role) const;
|
||||
|
||||
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
|
||||
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs = SongList());
|
||||
void ScheduleAddSongs(const SongList &songs);
|
||||
void ScheduleUpdateSongs(const SongList &songs);
|
||||
void ScheduleRemoveSongs(const SongList &songs);
|
||||
@@ -259,7 +264,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
|
||||
|
||||
private Q_SLOTS:
|
||||
void Reload();
|
||||
void ResetInternal();
|
||||
void ScheduleReset();
|
||||
void ProcessUpdate();
|
||||
void LoadSongsFromSqlAsyncFinished();
|
||||
@@ -278,7 +283,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
const SharedPtr<AlbumCoverLoader> albumcover_loader_;
|
||||
CollectionDirectoryModel *dir_model_;
|
||||
CollectionFilter *filter_;
|
||||
QTimer *timer_reload_;
|
||||
QTimer *timer_update_;
|
||||
|
||||
QPixmap pixmap_no_cover_;
|
||||
|
||||
@@ -25,12 +25,13 @@
|
||||
class CollectionModelUpdate {
|
||||
public:
|
||||
enum class Type {
|
||||
Reset,
|
||||
AddReAddOrUpdate,
|
||||
Add,
|
||||
Update,
|
||||
Remove,
|
||||
};
|
||||
explicit CollectionModelUpdate(const Type _type, const SongList &_songs);
|
||||
explicit CollectionModelUpdate(const Type _type, const SongList &_songs = SongList());
|
||||
Type type;
|
||||
SongList songs;
|
||||
};
|
||||
|
||||
@@ -58,4 +58,3 @@ class CollectionPlaylistItem : public PlaylistItem {
|
||||
};
|
||||
|
||||
#endif // COLLECTIONPLAYLISTITEM_H
|
||||
|
||||
|
||||
@@ -101,9 +101,9 @@ void CollectionQuery::AddCompilationRequirement(const bool compilation) {
|
||||
QString CollectionQuery::GetInnerQuery() const {
|
||||
return duplicates_only_
|
||||
? QStringLiteral(" INNER JOIN (select * from duplicated_songs) dsongs "
|
||||
"ON (%songs_table.artist = dsongs.dup_artist "
|
||||
"AND %songs_table.album = dsongs.dup_album "
|
||||
"AND %songs_table.title = dsongs.dup_title) ")
|
||||
"ON (%songs_table.artist = dsongs.dup_artist "
|
||||
"AND %songs_table.album = dsongs.dup_album "
|
||||
"AND %songs_table.title = dsongs.dup_title) ")
|
||||
: QString();
|
||||
}
|
||||
|
||||
|
||||
@@ -65,10 +65,8 @@
|
||||
#include "collectionitem.h"
|
||||
#include "collectionitemdelegate.h"
|
||||
#include "collectionview.h"
|
||||
#ifndef Q_OS_WIN32
|
||||
# include "device/devicemanager.h"
|
||||
# include "device/devicestatefiltermodel.h"
|
||||
#endif
|
||||
#include "device/devicemanager.h"
|
||||
#include "device/devicestatefiltermodel.h"
|
||||
#include "dialogs/edittagdialog.h"
|
||||
#include "dialogs/deleteconfirmationdialog.h"
|
||||
#include "organize/organizedialog.h"
|
||||
@@ -95,9 +93,7 @@ CollectionView::CollectionView(QWidget *parent)
|
||||
action_open_in_new_playlist_(nullptr),
|
||||
action_organize_(nullptr),
|
||||
action_search_for_this_(nullptr),
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_(nullptr),
|
||||
#endif
|
||||
action_edit_track_(nullptr),
|
||||
action_edit_tracks_(nullptr),
|
||||
action_rescan_songs_(nullptr),
|
||||
@@ -387,7 +383,7 @@ void CollectionView::keyPressEvent(QKeyEvent *e) {
|
||||
case Qt::Key_Enter:
|
||||
case Qt::Key_Return:
|
||||
if (currentIndex().isValid()) {
|
||||
AddToPlaylist();
|
||||
Q_EMIT doubleClicked(currentIndex());
|
||||
}
|
||||
e->accept();
|
||||
break;
|
||||
@@ -417,9 +413,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
context_menu_->addSeparator();
|
||||
action_organize_ = context_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &CollectionView::Organize);
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &CollectionView::CopyToDevice);
|
||||
#endif
|
||||
action_delete_files_ = context_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &CollectionView::Delete);
|
||||
|
||||
context_menu_->addSeparator();
|
||||
@@ -439,10 +433,8 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
context_menu_->addMenu(filter_widget_->menu());
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_->setDisabled(device_manager_->connected_devices_model()->rowCount() == 0);
|
||||
QObject::connect(device_manager_->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, action_copy_to_device_, &QAction::setDisabled);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@@ -481,9 +473,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
action_rescan_songs_->setEnabled(regular_editable > 0);
|
||||
|
||||
action_organize_->setVisible(regular_elements == regular_editable);
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_->setVisible(regular_elements == regular_editable);
|
||||
#endif
|
||||
|
||||
action_delete_files_->setVisible(delete_files_);
|
||||
|
||||
@@ -492,9 +482,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
// only when all selected items are editable
|
||||
action_organize_->setEnabled(regular_elements == regular_editable);
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
|
||||
#endif
|
||||
|
||||
action_delete_files_->setEnabled(delete_files_);
|
||||
|
||||
@@ -759,7 +747,6 @@ void CollectionView::RescanSongs() {
|
||||
|
||||
void CollectionView::CopyToDevice() {
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
if (!organize_dialog_) {
|
||||
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
|
||||
}
|
||||
@@ -768,7 +755,6 @@ void CollectionView::CopyToDevice() {
|
||||
organize_dialog_->SetCopy(true);
|
||||
organize_dialog_->SetSongs(GetSelectedSongs());
|
||||
organize_dialog_->show();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ class CollectionView : public AutoExpandingTreeView {
|
||||
void DeleteFilesFinished(const SongList &songs_with_errors);
|
||||
|
||||
private:
|
||||
void RecheckIsEmpty();
|
||||
void SetShowInVarious(const bool on);
|
||||
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
|
||||
void SaveContainerPath(const QModelIndex &child);
|
||||
@@ -176,9 +175,7 @@ class CollectionView : public AutoExpandingTreeView {
|
||||
QAction *action_organize_;
|
||||
QAction *action_search_for_this_;
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
QAction *action_copy_to_device_;
|
||||
#endif
|
||||
QAction *action_edit_track_;
|
||||
QAction *action_edit_tracks_;
|
||||
QAction *action_rescan_songs_;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -75,7 +75,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
QStringList CollectionWatcher::sValidImages = QStringList() << u"jpg"_s << u"png"_s << u"gif"_s << u"jpeg"_s;
|
||||
QStringList CollectionWatcher::sValidImages = QStringList() << u"jpg"_s << u"jpeg"_s << u"jp2"_s << u"png"_s << u"gif"_s << u"tiff"_s << u"tif"_s << u"webp"_s;
|
||||
|
||||
CollectionWatcher::CollectionWatcher(const Song::Source source,
|
||||
const SharedPtr<TaskManager> task_manager,
|
||||
@@ -261,7 +261,7 @@ void CollectionWatcher::ReloadSettings() {
|
||||
CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher, const int dir, const bool incremental, const bool ignores_mtime, const bool mark_songs_unavailable)
|
||||
: progress_(0),
|
||||
progress_max_(0),
|
||||
dir_(dir),
|
||||
dir_id_(dir),
|
||||
incremental_(incremental),
|
||||
ignores_mtime_(ignores_mtime),
|
||||
mark_songs_unavailable_(mark_songs_unavailable),
|
||||
@@ -313,6 +313,19 @@ void CollectionWatcher::ScanTransaction::AddToProgressMax(const quint64 n) {
|
||||
|
||||
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||
|
||||
if (!deleted_subdirs.isEmpty()) {
|
||||
Q_EMIT watcher_->SubdirsDeleted(deleted_subdirs);
|
||||
}
|
||||
|
||||
if (!new_subdirs.isEmpty()) {
|
||||
Q_EMIT watcher_->SubdirsDiscovered(new_subdirs);
|
||||
}
|
||||
|
||||
if (!touched_subdirs.isEmpty()) {
|
||||
Q_EMIT watcher_->SubdirsMTimeUpdated(touched_subdirs);
|
||||
touched_subdirs.clear();
|
||||
}
|
||||
|
||||
if (!deleted_songs.isEmpty()) {
|
||||
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
|
||||
Q_EMIT watcher_->SongsUnavailable(deleted_songs);
|
||||
@@ -338,34 +351,24 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||
readded_songs.clear();
|
||||
}
|
||||
|
||||
if (!new_subdirs.isEmpty()) {
|
||||
Q_EMIT watcher_->SubdirsDiscovered(new_subdirs);
|
||||
}
|
||||
|
||||
if (!touched_subdirs.isEmpty()) {
|
||||
Q_EMIT watcher_->SubdirsMTimeUpdated(touched_subdirs);
|
||||
touched_subdirs.clear();
|
||||
}
|
||||
|
||||
for (const CollectionSubdirectory &subdir : std::as_const(deleted_subdirs)) {
|
||||
if (watcher_->watched_dirs_.contains(dir_)) {
|
||||
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
|
||||
if (watcher_->watched_dirs_.contains(dir_id_)) {
|
||||
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_id_], subdir);
|
||||
}
|
||||
}
|
||||
deleted_subdirs.clear();
|
||||
|
||||
if (watcher_->monitor_) {
|
||||
// Watch the new subdirectories
|
||||
for (const CollectionSubdirectory &subdir : std::as_const(new_subdirs)) {
|
||||
if (watcher_->watched_dirs_.contains(dir_)) {
|
||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
|
||||
if (watcher_->watched_dirs_.contains(dir_id_)) {
|
||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_id_], subdir.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
new_subdirs.clear();
|
||||
|
||||
if (incremental_ || ignores_mtime_) {
|
||||
Q_EMIT watcher_->UpdateLastSeen(dir_, expire_unavailable_songs_days_);
|
||||
Q_EMIT watcher_->UpdateLastSeen(dir_id_, expire_unavailable_songs_days_);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -374,7 +377,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
|
||||
|
||||
if (cached_songs_dirty_) {
|
||||
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_);
|
||||
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_id_);
|
||||
for (const Song &song : songs) {
|
||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
cached_songs_.insert(p, song);
|
||||
@@ -393,7 +396,7 @@ SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QStri
|
||||
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QString &path) {
|
||||
|
||||
if (cached_songs_missing_fingerprint_dirty_) {
|
||||
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_);
|
||||
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_id_);
|
||||
for (const Song &song : songs) {
|
||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
cached_songs_missing_fingerprint_.insert(p, song);
|
||||
@@ -408,7 +411,7 @@ bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QS
|
||||
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingLoudnessCharacteristics(const QString &path) {
|
||||
|
||||
if (cached_songs_missing_loudness_characteristics_dirty_) {
|
||||
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_);
|
||||
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_id_);
|
||||
for (const Song &song : songs) {
|
||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
cached_songs_missing_loudness_characteristics_.insert(p, song);
|
||||
@@ -430,7 +433,7 @@ void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const CollectionSubdire
|
||||
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||
}
|
||||
|
||||
return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const CollectionSubdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; });
|
||||
@@ -440,7 +443,7 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||
}
|
||||
|
||||
CollectionSubdirectoryList ret;
|
||||
@@ -457,7 +460,7 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdi
|
||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||
}
|
||||
|
||||
return known_subdirs_;
|
||||
@@ -494,7 +497,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
||||
const quint64 files_count = FilesCountForPath(&transaction, dir.path);
|
||||
transaction.SetKnownSubdirs(subdirs);
|
||||
transaction.AddToProgressMax(files_count);
|
||||
ScanSubdirectory(dir.path, CollectionSubdirectory(), files_count, &transaction);
|
||||
ScanSubdirectory(dir, dir.path, CollectionSubdirectory(), files_count, &transaction);
|
||||
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
||||
}
|
||||
else {
|
||||
@@ -512,7 +515,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
||||
transaction.AddToProgressMax(files_count);
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
}
|
||||
if (!stop_or_abort_requested()) {
|
||||
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
||||
@@ -524,9 +527,10 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
|
||||
void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
|
||||
|
||||
const QFileInfo path_info(path);
|
||||
const qint64 path_mtime = path_info.exists() && path_info.lastModified().isValid() ? path_info.lastModified().toSecsSinceEpoch() : 0;
|
||||
|
||||
if (path_info.isSymLink()) {
|
||||
const QString real_path = path_info.symLinkTarget();
|
||||
@@ -536,8 +540,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
return;
|
||||
}
|
||||
// Do not scan symlinked dirs that are already in collection
|
||||
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
|
||||
if (real_path.startsWith(dir.path)) {
|
||||
for (const CollectionDirectory &i : std::as_const(watched_dirs_)) {
|
||||
if (real_path.startsWith(i.path)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -563,7 +567,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() && subdir.mtime == path_info.lastModified().toSecsSinceEpoch() && !songs_missing_fingerprint && !songs_missing_loudness_characteristics) {
|
||||
if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() && path_mtime != 0 && subdir.mtime == path_mtime && !songs_missing_fingerprint && !songs_missing_loudness_characteristics) {
|
||||
// The directory hasn't changed since last time
|
||||
t->AddToProgress(files_count);
|
||||
return;
|
||||
@@ -578,53 +582,52 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
const CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
||||
for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
|
||||
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
|
||||
ScanSubdirectory(prev_subdir.path, prev_subdir, 0, t, true);
|
||||
ScanSubdirectory(dir, prev_subdir.path, prev_subdir, 0, t, true);
|
||||
}
|
||||
}
|
||||
|
||||
// First we "quickly" get a list of the files in the directory that we think might be music. While we're here, we also look for new subdirectories and possible album artwork.
|
||||
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||
while (it.hasNext()) {
|
||||
if (path_info.exists()) {
|
||||
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||
while (it.hasNext()) {
|
||||
|
||||
if (stop_or_abort_requested()) return;
|
||||
if (stop_or_abort_requested()) return;
|
||||
|
||||
const QString child_filepath = it.next();
|
||||
const QFileInfo child_fileinfo(child_filepath);
|
||||
const QString child_filepath = it.next();
|
||||
const QFileInfo child_fileinfo(child_filepath);
|
||||
|
||||
if (child_fileinfo.isSymLink()) {
|
||||
QStorageInfo storage_info(child_fileinfo.symLinkTarget());
|
||||
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
|
||||
qLog(Warning) << "Ignoring symbolic link" << child_filepath << "which links to" << child_fileinfo.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
|
||||
continue;
|
||||
if (child_fileinfo.isSymLink()) {
|
||||
const QStorageInfo storage_info(child_fileinfo.symLinkTarget());
|
||||
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
|
||||
qLog(Warning) << "Ignoring symbolic link" << child_filepath << "which links to" << child_fileinfo.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (child_fileinfo.isDir()) {
|
||||
if (!t->HasSeenSubdir(child_filepath)) {
|
||||
// We haven't seen this subdirectory before - add it to a list, and later we'll tell the backend about it and scan it.
|
||||
CollectionSubdirectory new_subdir;
|
||||
new_subdir.directory_id = -1;
|
||||
new_subdir.path = child_filepath;
|
||||
new_subdir.mtime = child_fileinfo.lastModified().toSecsSinceEpoch();
|
||||
my_new_subdirs << new_subdir;
|
||||
}
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else {
|
||||
QString ext_part(ExtensionPart(child_filepath));
|
||||
QString dir_part(DirectoryPart(child_filepath));
|
||||
if (Song::kRejectedExtensions.contains(child_fileinfo.suffix(), Qt::CaseInsensitive) || child_fileinfo.baseName() == "qt_temp"_L1) {
|
||||
if (child_fileinfo.isDir()) {
|
||||
if (!t->HasSeenSubdir(child_filepath)) {
|
||||
// We haven't seen this subdirectory before - add it to a list, and later we'll tell the backend about it and scan it.
|
||||
CollectionSubdirectory new_subdir;
|
||||
new_subdir.directory_id = -1;
|
||||
new_subdir.path = child_filepath;
|
||||
new_subdir.mtime = child_fileinfo.exists() && child_fileinfo.lastModified().isValid() ? child_fileinfo.lastModified().toSecsSinceEpoch() : 0;
|
||||
my_new_subdirs << new_subdir;
|
||||
}
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else if (sValidImages.contains(ext_part)) {
|
||||
album_art[dir_part] << child_filepath;
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else if (tagreader_client_->IsMediaFileBlocking(child_filepath)) {
|
||||
files_on_disk << child_filepath;
|
||||
}
|
||||
else {
|
||||
t->AddToProgress(1);
|
||||
const QString ext_part = ExtensionPart(child_filepath);
|
||||
const QString dir_part = DirectoryPart(child_filepath);
|
||||
if (Song::kRejectedExtensions.contains(child_fileinfo.suffix(), Qt::CaseInsensitive) || child_fileinfo.baseName() == "qt_temp"_L1) {
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else if (sValidImages.contains(ext_part)) {
|
||||
album_art[dir_part] << child_filepath;
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else {
|
||||
files_on_disk << child_filepath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -632,27 +635,27 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
if (stop_or_abort_requested()) return;
|
||||
|
||||
// Ask the database for a list of files in this directory
|
||||
SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
||||
const SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
||||
|
||||
QSet<QString> cues_processed;
|
||||
|
||||
// Now compare the list from the database with the list of files on disk
|
||||
QStringList files_on_disk_copy = files_on_disk;
|
||||
const QStringList files_on_disk_copy = files_on_disk;
|
||||
for (const QString &file : files_on_disk_copy) {
|
||||
|
||||
if (stop_or_abort_requested()) return;
|
||||
|
||||
// Associated CUE
|
||||
QString new_cue = CueParser::FindCueFilename(file);
|
||||
const QString new_cue = CueParser::FindCueFilename(file);
|
||||
|
||||
SongList matching_songs;
|
||||
if (FindSongsByPath(songs_in_db, file, &matching_songs)) { // Found matching song in DB by path.
|
||||
|
||||
Song matching_song = matching_songs.first();
|
||||
const Song matching_song = matching_songs.first();
|
||||
|
||||
// The song is in the database and still on disk.
|
||||
// Check the mtime to see if it's been changed since it was added.
|
||||
QFileInfo fileinfo(file);
|
||||
const QFileInfo fileinfo(file);
|
||||
|
||||
if (!fileinfo.exists()) {
|
||||
// Partially fixes race condition - if file was removed between being added to the list and now.
|
||||
@@ -706,8 +709,14 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
qLog(Debug) << file << "is missing EBU R 128 loudness characteristics.";
|
||||
}
|
||||
|
||||
// If the song is unavailable and nothing has changed, just mark it as available without re-scanning
|
||||
// For CUE files with multiple sections, all sections share the same file and would have the same availability status
|
||||
if (matching_song.unavailable() && !changed && !missing_fingerprint && !missing_loudness_characteristics) {
|
||||
qLog(Debug) << "Unavailable song" << file << "restored without re-scanning.";
|
||||
t->readded_songs << matching_songs;
|
||||
}
|
||||
// The song's changed or missing fingerprint - create fingerprint and reread the metadata from file.
|
||||
if (t->ignores_mtime() || changed || missing_fingerprint || missing_loudness_characteristics) {
|
||||
else if (t->ignores_mtime() || changed || missing_fingerprint || missing_loudness_characteristics) {
|
||||
|
||||
QString fingerprint;
|
||||
#ifdef HAVE_SONGFINGERPRINTING
|
||||
@@ -721,19 +730,15 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
#endif
|
||||
|
||||
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
|
||||
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t);
|
||||
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t)) {
|
||||
files_on_disk.removeAll(file);
|
||||
}
|
||||
}
|
||||
else { // If CUE associated.
|
||||
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing has changed - mark the song available without re-scanning
|
||||
else if (matching_song.unavailable()) {
|
||||
qLog(Debug) << "Unavailable song" << file << "restored.";
|
||||
t->readded_songs << matching_songs;
|
||||
}
|
||||
|
||||
}
|
||||
else { // Search the DB by fingerprint.
|
||||
QString fingerprint;
|
||||
@@ -750,7 +755,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
|
||||
// The song is in the database and still on disk.
|
||||
// Check the mtime to see if it's been changed since it was added.
|
||||
QFileInfo fileinfo(file);
|
||||
const QFileInfo fileinfo(file);
|
||||
if (!fileinfo.exists()) {
|
||||
// Partially fixes race condition - if file was removed between being added to the list and now.
|
||||
files_on_disk.removeAll(file);
|
||||
@@ -761,7 +766,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
// Make sure the songs aren't deleted, as they still exist elsewhere with a different file path.
|
||||
bool matching_songs_has_cue = false;
|
||||
for (const Song &matching_song : std::as_const(matching_songs)) {
|
||||
QString matching_filename = matching_song.url().toLocalFile();
|
||||
const QString matching_filename = matching_song.url().toLocalFile();
|
||||
if (!t->files_changed_path_.contains(matching_filename)) {
|
||||
t->files_changed_path_ << matching_filename;
|
||||
qLog(Debug) << matching_filename << "has changed path to" << file;
|
||||
@@ -784,7 +789,9 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
const QUrl art_automatic = ArtForSong(file, album_art);
|
||||
|
||||
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
|
||||
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t);
|
||||
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t)) {
|
||||
files_on_disk.removeAll(file);
|
||||
}
|
||||
}
|
||||
else { // If CUE associated.
|
||||
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
||||
@@ -795,6 +802,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
|
||||
const SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
|
||||
if (songs.isEmpty()) {
|
||||
files_on_disk.removeAll(file);
|
||||
t->AddToProgress(1);
|
||||
continue;
|
||||
}
|
||||
@@ -805,7 +813,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
const QUrl art_automatic = ArtForSong(file, album_art);
|
||||
|
||||
for (Song song : songs) {
|
||||
song.set_directory_id(t->dir());
|
||||
song.set_directory_id(t->dir_id());
|
||||
if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic);
|
||||
t->new_songs << song;
|
||||
}
|
||||
@@ -823,27 +831,26 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
}
|
||||
}
|
||||
|
||||
// Add this subdir to the new or touched list
|
||||
// Add, update or delete subdir
|
||||
CollectionSubdirectory updated_subdir;
|
||||
updated_subdir.directory_id = t->dir();
|
||||
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
|
||||
updated_subdir.directory_id = t->dir_id();
|
||||
updated_subdir.mtime = path_mtime;
|
||||
updated_subdir.path = path;
|
||||
|
||||
if (subdir.directory_id == -1) {
|
||||
if (!path_info.exists() && updated_subdir.path != dir.path) {
|
||||
t->deleted_subdirs << updated_subdir;
|
||||
}
|
||||
else if (subdir.directory_id == -1) {
|
||||
t->new_subdirs << updated_subdir;
|
||||
}
|
||||
else {
|
||||
else if (subdir.mtime != updated_subdir.mtime) {
|
||||
t->touched_subdirs << updated_subdir;
|
||||
}
|
||||
|
||||
if (updated_subdir.mtime == 0) { // CollectionSubdirectory deleted, mark it for removal from the watcher.
|
||||
t->deleted_subdirs << updated_subdir;
|
||||
}
|
||||
|
||||
// Recurse into the new subdirs that we found
|
||||
for (const CollectionSubdirectory &my_new_subdir : std::as_const(my_new_subdirs)) {
|
||||
if (stop_or_abort_requested()) return;
|
||||
ScanSubdirectory(my_new_subdir.path, my_new_subdir, 0, t, true);
|
||||
ScanSubdirectory(dir, my_new_subdir.path, my_new_subdir, 0, t, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -875,7 +882,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
|
||||
QSet<int> used_ids;
|
||||
for (Song new_cue_song : songs) {
|
||||
new_cue_song.set_source(source_);
|
||||
new_cue_song.set_directory_id(t->dir());
|
||||
new_cue_song.set_directory_id(t->dir_id());
|
||||
PerformEBUR128Analysis(new_cue_song);
|
||||
new_cue_song.set_fingerprint(fingerprint);
|
||||
|
||||
@@ -901,7 +908,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
const QString &fingerprint,
|
||||
const SongList &matching_songs,
|
||||
const QUrl &art_automatic,
|
||||
@@ -922,7 +929,7 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
const TagReaderResult result = tagreader_client_->ReadFileBlocking(file, &song_on_disk);
|
||||
if (result.success() && song_on_disk.is_valid()) {
|
||||
song_on_disk.set_source(source_);
|
||||
song_on_disk.set_directory_id(t->dir());
|
||||
song_on_disk.set_directory_id(t->dir_id());
|
||||
song_on_disk.set_id(matching_song.id());
|
||||
PerformEBUR128Analysis(song_on_disk);
|
||||
song_on_disk.set_fingerprint(fingerprint);
|
||||
@@ -931,6 +938,8 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
AddChangedSong(file, matching_song, song_on_disk, t);
|
||||
}
|
||||
|
||||
return result.success() && song_on_disk.is_valid();
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const {
|
||||
@@ -1199,12 +1208,13 @@ void CollectionWatcher::DirectoryChanged(const QString &subdir) {
|
||||
|
||||
void CollectionWatcher::RescanPathsNow() {
|
||||
|
||||
const QList<int> dirs = rescan_queue_.keys();
|
||||
for (const int dir : dirs) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
ScanTransaction transaction(this, dir, false, false, mark_songs_unavailable_);
|
||||
const QList<int> dir_ids = rescan_queue_.keys();
|
||||
for (const int dir_id : dir_ids) {
|
||||
|
||||
const QStringList paths = rescan_queue_.value(dir);
|
||||
if (stop_or_abort_requested()) break;
|
||||
ScanTransaction transaction(this, dir_id, false, false, mark_songs_unavailable_);
|
||||
|
||||
const QStringList paths = rescan_queue_.value(dir_id);
|
||||
|
||||
QMap<QString, quint64> subdir_files_count;
|
||||
for (const QString &path : paths) {
|
||||
@@ -1215,11 +1225,14 @@ void CollectionWatcher::RescanPathsNow() {
|
||||
|
||||
for (const QString &path : paths) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
if (!subdir_mapping_.contains(path)) {
|
||||
continue;
|
||||
}
|
||||
CollectionSubdirectory subdir;
|
||||
subdir.directory_id = dir;
|
||||
subdir.directory_id = dir_id;
|
||||
subdir.mtime = 0;
|
||||
subdir.path = path;
|
||||
ScanSubdirectory(path, subdir, subdir_files_count[path], &transaction);
|
||||
ScanSubdirectory(subdir_mapping_[path], path, subdir, subdir_files_count[path], &transaction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1344,11 +1357,13 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
|
||||
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_);
|
||||
CollectionSubdirectoryList subdirs = transaction.GetAllSubdirs();
|
||||
|
||||
if (subdirs.isEmpty()) {
|
||||
qLog(Debug) << "Collection directory wasn't in subdir list.";
|
||||
const bool has_collection_root_dir = std::any_of(subdirs.begin(), subdirs.end(), [&dir](const CollectionSubdirectory &subdir) { return subdir.path == dir.path; });
|
||||
if (!has_collection_root_dir) {
|
||||
qLog(Debug) << "Collection directory wasn't in subdir list, re-adding";
|
||||
CollectionSubdirectory subdir;
|
||||
subdir.path = dir.path;
|
||||
subdir.directory_id = dir.id;
|
||||
subdir.path = dir.path;
|
||||
subdir.mtime = 0;
|
||||
subdirs << subdir;
|
||||
}
|
||||
|
||||
@@ -1358,7 +1373,7 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
|
||||
|
||||
for (const CollectionSubdirectory &subdir : std::as_const(subdirs)) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1459,6 +1474,8 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
|
||||
QStringList scanned_paths;
|
||||
for (const Song &song : songs) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
if (!watched_dirs_.contains(song.directory_id())) continue;
|
||||
const CollectionDirectory dir = watched_dirs_[song.directory_id()];
|
||||
const QString song_path = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
if (scanned_paths.contains(song_path)) continue;
|
||||
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
|
||||
@@ -1468,7 +1485,7 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
|
||||
if (subdir.path != song_path) continue;
|
||||
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
|
||||
const quint64 files_count = FilesCountForPath(&transaction, subdir.path);
|
||||
ScanSubdirectory(song_path, subdir, files_count, &transaction);
|
||||
ScanSubdirectory(dir, song_path, subdir, files_count, &transaction);
|
||||
scanned_paths << subdir.path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -85,6 +85,7 @@ class CollectionWatcher : public QObject {
|
||||
void SongsReadded(const SongList &songs, const bool unavailable = false);
|
||||
void SubdirsDiscovered(const CollectionSubdirectoryList &subdirs);
|
||||
void SubdirsMTimeUpdated(const CollectionSubdirectoryList &subdirs);
|
||||
void SubdirsDeleted(const CollectionSubdirectoryList &subdirs);
|
||||
void CompilationsNeedUpdating();
|
||||
void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
|
||||
void ExitFinished();
|
||||
@@ -122,7 +123,7 @@ class CollectionWatcher : public QObject {
|
||||
// Emits the signals for new & deleted songs etc and clears the lists. This causes the new stuff to be updated on UI.
|
||||
void CommitNewOrUpdatedSongs();
|
||||
|
||||
int dir() const { return dir_; }
|
||||
int dir_id() const { return dir_id_; }
|
||||
bool is_incremental() const { return incremental_; }
|
||||
bool ignores_mtime() const { return ignores_mtime_; }
|
||||
|
||||
@@ -137,13 +138,13 @@ class CollectionWatcher : public QObject {
|
||||
QStringList files_changed_path_;
|
||||
|
||||
private:
|
||||
ScanTransaction &operator=(const ScanTransaction&) { return *this; }
|
||||
ScanTransaction &operator=(const ScanTransaction &transaction) { Q_UNUSED(transaction); return *this; }
|
||||
|
||||
int task_id_;
|
||||
quint64 progress_;
|
||||
quint64 progress_max_;
|
||||
|
||||
int dir_;
|
||||
int dir_id_;
|
||||
// Incremental scan enters a directory only if it has changed since the last scan.
|
||||
bool incremental_;
|
||||
// This type of scan updates every file in a folder that's being scanned.
|
||||
@@ -179,7 +180,7 @@ class CollectionWatcher : public QObject {
|
||||
void IncrementalScanNow();
|
||||
void FullScanNow();
|
||||
void RescanPathsNow();
|
||||
void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
|
||||
void ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
|
||||
void RescanSongs(const SongList &songs);
|
||||
|
||||
private:
|
||||
@@ -202,7 +203,7 @@ class CollectionWatcher : public QObject {
|
||||
// Updates the sections of a cue associated and altered (according to mtime) media file during a scan.
|
||||
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &art_automatic, const SongList &old_cue_songs, ScanTransaction *t) const;
|
||||
// Updates a single non-cue associated and altered (according to mtime) song during a scan.
|
||||
void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
|
||||
bool UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
|
||||
// Scans a single media file that's present on the disk but not yet in the collection.
|
||||
// It may result in a multiple files added to the collection when the media file has many sections (like a CUE related media file).
|
||||
SongList ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const;
|
||||
@@ -261,7 +262,6 @@ class CollectionWatcher : public QObject {
|
||||
static QStringList sValidImages;
|
||||
|
||||
qint64 last_scan_time_;
|
||||
|
||||
};
|
||||
|
||||
inline QString CollectionWatcher::NoExtensionPart(const QString &fileName) {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QIODevice>
|
||||
#include <QDataStream>
|
||||
#include <QKeySequence>
|
||||
@@ -167,14 +168,20 @@ void SavedGroupingManager::UpdateModel() {
|
||||
if (version == 1) {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(name).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
ds >> g;
|
||||
|
||||
QList<QStandardItem*> list;
|
||||
list << new QStandardItem(saved.at(i))
|
||||
|
||||
QStandardItem *item = new QStandardItem();
|
||||
item->setText(QUrl::fromPercentEncoding(name.toUtf8()));
|
||||
item->setData(name);
|
||||
|
||||
list << item
|
||||
<< new QStandardItem(GroupByToString(g.first))
|
||||
<< new QStandardItem(GroupByToString(g.second))
|
||||
<< new QStandardItem(GroupByToString(g.third));
|
||||
@@ -185,8 +192,9 @@ void SavedGroupingManager::UpdateModel() {
|
||||
else {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
s.remove(saved.at(i));
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
s.remove(name);
|
||||
}
|
||||
}
|
||||
s.endGroup();
|
||||
@@ -202,7 +210,7 @@ void SavedGroupingManager::Remove() {
|
||||
for (const QModelIndex &idx : indexes) {
|
||||
if (idx.isValid()) {
|
||||
qLog(Debug) << "Remove saved grouping: " << model_->item(idx.row(), 0)->text();
|
||||
s.remove(model_->item(idx.row(), 0)->text());
|
||||
s.remove(model_->item(idx.row(), 0)->data().toString());
|
||||
}
|
||||
}
|
||||
s.endGroup();
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef APPEARANCESETTINGS_H
|
||||
#define APPEARANCESETTINGS_H
|
||||
@@ -70,6 +70,6 @@ enum class BackgroundImagePosition {
|
||||
BottomRight = 5
|
||||
};
|
||||
|
||||
} // namespace
|
||||
} // namespace AppearanceSettings
|
||||
|
||||
#endif // APPEARANCESETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef BACKENDSETTINGS_H
|
||||
#define BACKENDSETTINGS_H
|
||||
@@ -63,6 +63,6 @@ constexpr qint64 kDefaultBufferDuration = 4000;
|
||||
constexpr double kDefaultBufferLowWatermark = 0.33;
|
||||
constexpr double kDefaultBufferHighWatermark = 0.99;
|
||||
|
||||
} // namespace
|
||||
} // namespace BackendSettings
|
||||
|
||||
#endif // BACKENDSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef BEHAVIOURSETTINGS_H
|
||||
#define BEHAVIOURSETTINGS_H
|
||||
@@ -71,6 +71,6 @@ constexpr char kDoubleClickPlaylistAddMode[] = "doubleclick_playlist_addmode";
|
||||
constexpr char kSeekStepSec[] = "seek_step_sec";
|
||||
constexpr char kVolumeIncrement[] = "volume_increment";
|
||||
|
||||
} // namespace
|
||||
} // namespace BehaviourSettings
|
||||
|
||||
#endif // BEHAVIOURSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONSETTINGS_H
|
||||
#define COLLECTIONSETTINGS_H
|
||||
@@ -24,18 +24,20 @@ namespace CollectionSettings {
|
||||
|
||||
constexpr char kSettingsGroup[] = "Collection";
|
||||
|
||||
constexpr char kStartupScan[] = "startup_scan";
|
||||
constexpr char kMonitor[] = "monitor";
|
||||
constexpr char kSongTracking[] = "song_tracking";
|
||||
constexpr char kMarkSongsUnavailable[] = "mark_songs_unavailable";
|
||||
constexpr char kSongENUR128LoudnessAnalysis[] = "song_ebur128_loudness_analysis";
|
||||
constexpr char kExpireUnavailableSongs[] = "expire_unavailable_songs";
|
||||
constexpr char kCoverArtPatterns[] = "cover_art_patterns";
|
||||
constexpr char kAutoOpen[] = "auto_open";
|
||||
constexpr char kShowDividers[] = "show_dividers";
|
||||
constexpr char kPrettyCovers[] = "pretty_covers";
|
||||
constexpr char kVariousArtists[] = "various_artists";
|
||||
constexpr char kSortSkipsArticles[] = "sort_skips_articles";
|
||||
constexpr char kStartupScan[] = "startup_scan";
|
||||
constexpr char kMonitor[] = "monitor";
|
||||
constexpr char kSongTracking[] = "song_tracking";
|
||||
constexpr char kSongENUR128LoudnessAnalysis[] = "song_ebur128_loudness_analysis";
|
||||
constexpr char kMarkSongsUnavailable[] = "mark_songs_unavailable";
|
||||
constexpr char kExpireUnavailableSongs[] = "expire_unavailable_songs";
|
||||
constexpr char kCoverArtPatterns[] = "cover_art_patterns";
|
||||
constexpr char kSkipArticlesForArtists[] = "skip_articles_for_artists";
|
||||
constexpr char kSkipArticlesForAlbums[] = "skip_articles_for_albums";
|
||||
constexpr char kUseSortTags[] = "use_short_tags";
|
||||
constexpr char kSettingsCacheSize[] = "cache_size";
|
||||
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
|
||||
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";
|
||||
@@ -57,6 +59,6 @@ enum class CacheSizeUnit {
|
||||
TB
|
||||
};
|
||||
|
||||
} // namespace
|
||||
} // namespace CollectionSettings
|
||||
|
||||
#endif // COLLECTIONSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef CONTEXTSETTINGS_H
|
||||
#define CONTEXTSETTINGS_H
|
||||
@@ -43,6 +43,6 @@ constexpr char kSettingsSummaryFmt[] = "SummaryFmt";
|
||||
constexpr char kDefaultFontFamily[] = "Noto Sans";
|
||||
constexpr qreal kDefaultFontSizeHeadline = 11;
|
||||
|
||||
} // namespace
|
||||
} // namespace ContextSettings
|
||||
|
||||
#endif // CONTEXTSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COVERSSETTINGS_H
|
||||
#define COVERSSETTINGS_H
|
||||
@@ -32,6 +32,6 @@ constexpr char kSaveOverwrite[] = "save_overwrite";
|
||||
constexpr char kSaveLowercase[] = "save_lowercase";
|
||||
constexpr char kSaveReplaceSpaces[] = "save_replace_spaces";
|
||||
|
||||
} // namespace
|
||||
} // namespace CoversSettings
|
||||
|
||||
#endif // COVERSSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILEFILTERCONSTANTS_H
|
||||
#define FILEFILTERCONSTANTS_H
|
||||
@@ -27,13 +27,13 @@ constexpr char kAllFilesFilterSpec[] = QT_TRANSLATE_NOOP("FileFilter", "All File
|
||||
constexpr char kFileFilter[] =
|
||||
"*.wav *.flac *.wv *.ogg *.oga *.opus *.spx *.ape *.mpc "
|
||||
"*.mp2 *.mp3 *.m4a *.mp4 *.aac *.asf *.asx *.wma "
|
||||
"*.aif *.aiff *.mka *.tta *.dsf *.dsd "
|
||||
"*.aif *.aiff *.mka *.tta *.dsf *.dsd *.webm "
|
||||
"*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini "
|
||||
"*.ac3 *.dts "
|
||||
"*.mod *.s3m *.xm *.it"
|
||||
"*.mod *.s3m *.xm *.it "
|
||||
"*.spc *.vgm";
|
||||
|
||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
|
||||
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
|
||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm *.webp)");
|
||||
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm *.webp)");
|
||||
|
||||
#endif // FILEFILTERCONSTANTS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILENAMECONSTANTS_H
|
||||
#define FILENAMECONSTANTS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILESYSTEMCONSTANTS_H
|
||||
#define FILESYSTEMCONSTANTS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GLOBALSHORTCUTSSETTINGS_H
|
||||
#define GLOBALSHORTCUTSSETTINGS_H
|
||||
@@ -26,6 +26,6 @@ constexpr char kSettingsGroup[] = "GlobalShortcuts";
|
||||
constexpr char kUseKGlobalAccel[] = "use_kglobalaccel";
|
||||
constexpr char kUseX11[] = "use_x11";
|
||||
|
||||
} // namespace
|
||||
} // namespace GlobalShortcutsSettings
|
||||
|
||||
#endif // GLOBALSHORTCUTSSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef LYRICSSETTINGS_H
|
||||
#define LYRICSSETTINGS_H
|
||||
@@ -25,6 +25,6 @@ namespace LyricsSettings {
|
||||
constexpr char kSettingsGroup[] = "Lyrics";
|
||||
constexpr char kProviders[] = "providers";
|
||||
|
||||
} // namespace
|
||||
} // namespace LyricsSettings
|
||||
|
||||
#endif // LYRICSSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MAINWINDOWSETTINGS_H
|
||||
#define MAINWINDOWSETTINGS_H
|
||||
@@ -32,6 +32,6 @@ constexpr char kGeometry[] = "geometry";
|
||||
constexpr char kSplitterState[] = "splitter_state";
|
||||
constexpr char kDoNotShowSponsorMessage[] = "do_not_show_sponsor_message";
|
||||
|
||||
} // namespace
|
||||
} // namespace MainWindowSettings
|
||||
|
||||
#endif // MAINWINDOWSETTINGS_H
|
||||
#endif // MAINWINDOWSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MOODBARSETTINGS_H
|
||||
#define MOODBARSETTINGS_H
|
||||
@@ -38,6 +38,6 @@ constexpr char kShow[] = "show";
|
||||
constexpr char kStyle[] = "style";
|
||||
constexpr char kSave[] = "save";
|
||||
|
||||
} // namespace
|
||||
} // namespace MoodbarSettings
|
||||
|
||||
#endif // MOODBARSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef NETWORKPROXYSETTINGS_H
|
||||
#define NETWORKPROXYSETTINGS_H
|
||||
@@ -32,6 +32,6 @@ constexpr char kUsername[] = "username";
|
||||
constexpr char kPassword[] = "password";
|
||||
constexpr char kEngine[] = "engine";
|
||||
|
||||
} // namespace
|
||||
} // namespace NetworkProxySettings
|
||||
|
||||
#endif // NETWORKPROXYSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef NOTIFICATIONSSETTINGS_H
|
||||
#define NOTIFICATIONSSETTINGS_H
|
||||
@@ -45,7 +45,7 @@ constexpr char kCustomTextEnabled[] = "CustomTextEnabled";
|
||||
constexpr char kCustomText1[] = "CustomText1";
|
||||
constexpr char kCustomText2[] = "CustomText2";
|
||||
|
||||
} // namespace
|
||||
} // namespace OSDSettings
|
||||
|
||||
namespace OSDPrettySettings {
|
||||
|
||||
@@ -63,7 +63,7 @@ constexpr char kFading[] = "fading";
|
||||
constexpr QRgb kPresetBlue = qRgb(102, 150, 227);
|
||||
constexpr QRgb kPresetRed = qRgb(202, 22, 16);
|
||||
|
||||
} // namespace
|
||||
} // namespace OSDPrettySettings
|
||||
|
||||
namespace DiscordRPCSettings {
|
||||
|
||||
@@ -71,6 +71,14 @@ constexpr char kSettingsGroup[] = "DiscordRPC";
|
||||
|
||||
constexpr char kEnabled[] = "enabled";
|
||||
|
||||
} // namespace
|
||||
constexpr char kStatusDisplayType[] = "StatusDisplayType";
|
||||
|
||||
enum class StatusDisplayType {
|
||||
App = 0,
|
||||
Artist,
|
||||
Song
|
||||
};
|
||||
|
||||
} // namespace DiscordRPCSettings
|
||||
|
||||
#endif // NOTIFICATIONSSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLAYLISTSETTINGS_H
|
||||
#define PLAYLISTSETTINGS_H
|
||||
@@ -63,7 +63,7 @@ constexpr char kLastSaveExtension[] = "last_save_extension";
|
||||
constexpr char kLastSaveAllPath[] = "last_save_all_path";
|
||||
constexpr char kLastSaveAllExtension[] = "last_save_all_extension";
|
||||
|
||||
} // namespace
|
||||
} // namespace PlaylistSettings
|
||||
|
||||
Q_DECLARE_METATYPE(PlaylistSettings::PathType)
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef QOBUZSETTINGS_H
|
||||
#define QOBUZSETTINGS_H
|
||||
@@ -36,12 +36,13 @@ constexpr char kAlbumsSearchLimit[] = "albumssearchlimit";
|
||||
constexpr char kSongsSearchLimit[] = "songssearchlimit";
|
||||
constexpr char kBase64Secret[] = "base64secret";
|
||||
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
|
||||
constexpr char kRemoveRemastered[] = "remove_remastered";
|
||||
|
||||
constexpr char kUserId[] = "user_id";
|
||||
constexpr char kCredentialsId[] = "credentials_id";
|
||||
constexpr char kDeviceId[] = "device_id";
|
||||
constexpr char kUserAuthToken[] = "user_auth_token";
|
||||
|
||||
} // namespace
|
||||
} // namespace QobuzSettings
|
||||
|
||||
#endif // QOBUZSETTINGS_H
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user