Compare commits
363 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4735e8feea | ||
|
|
2acd94a04a | ||
|
|
9f2961c46c | ||
|
|
73a4a673fe | ||
|
|
0f071c8b31 | ||
| 08fe6d7ebb | |||
| bea094cbf1 | |||
| 0bea764b9f | |||
| 8d49b87b7c | |||
| 7a954b3f32 | |||
| d4d805443e | |||
| 833ae4fe72 | |||
| c26e09e90b | |||
| a30b4c1ac2 | |||
| d32ff688eb | |||
| 06dc5d0499 | |||
| bd59c19301 | |||
| 6a1d8bbc87 | |||
| 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 | ||
|
|
83c666baf9 | ||
|
|
b9b54e6e96 | ||
|
|
b2ff6240eb | ||
|
|
26a7c74a24 | ||
|
|
a34954ec4a | ||
|
|
349ab62e75 | ||
|
|
65e960f2c5 | ||
|
|
e22fef8ca4 | ||
|
|
3e99045e2c | ||
|
|
4fcade273e | ||
|
|
5eaff0d26e | ||
|
|
5b22f12b4a | ||
|
|
5f85c2e7a5 | ||
|
|
4fb5a7b6bc | ||
|
|
04c6c862c4 | ||
|
|
baec45f742 | ||
|
|
9efdbd2c10 | ||
|
|
d8800b80d5 | ||
|
|
ec715abb0d | ||
|
|
1485801efb | ||
|
|
4f9ac3d33a | ||
|
|
1577ce4d67 | ||
|
|
7eee74a2e9 | ||
|
|
d9e38fb3be | ||
|
|
81cc90e54a | ||
|
|
bd9771a88f | ||
|
|
f5cd81fe09 | ||
|
|
277e2cff59 | ||
|
|
6fa9514059 | ||
|
|
c5e38b71f7 | ||
|
|
3746915ae7 |
@@ -130,7 +130,10 @@ InsertBraces: false
|
|||||||
InsertTrailingCommas: None
|
InsertTrailingCommas: None
|
||||||
JavaScriptQuotes: Leave
|
JavaScriptQuotes: Leave
|
||||||
JavaScriptWrapImports: true
|
JavaScriptWrapImports: true
|
||||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
KeepEmptyLines:
|
||||||
|
AtEndOfFile: true
|
||||||
|
AtStartOfBlock: true
|
||||||
|
AtStartOfFile: true
|
||||||
LambdaBodyIndentation: Signature
|
LambdaBodyIndentation: Signature
|
||||||
MacroBlockBegin: ''
|
MacroBlockBegin: ''
|
||||||
MacroBlockEnd: ''
|
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
|
|
||||||
183
.github/workflows/build.yml
vendored
183
.github/workflows/build.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
opensuse_version: [ 'tumbleweed', 'leap:15.6' ]
|
opensuse_version: [ 'tumbleweed', 'leap:15.6', 'leap:16.0' ]
|
||||||
container:
|
container:
|
||||||
image: opensuse/${{matrix.opensuse_version}}
|
image: opensuse/${{matrix.opensuse_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -27,11 +27,11 @@ jobs:
|
|||||||
- name: Upgrade packages (Leap)
|
- name: Upgrade packages (Leap)
|
||||||
if: matrix.opensuse_version != 'tumbleweed'
|
if: matrix.opensuse_version != 'tumbleweed'
|
||||||
run: zypper -n --gpg-auto-import-keys up
|
run: zypper -n --gpg-auto-import-keys up
|
||||||
- name: Install gcc (Tumbleweed)
|
- name: Install gcc
|
||||||
if: matrix.opensuse_version == 'tumbleweed'
|
if: matrix.opensuse_version != 'leap:15.6'
|
||||||
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
|
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
|
||||||
- name: Install gcc (Leap)
|
- name: Install gcc (leap:15.6)
|
||||||
if: matrix.opensuse_version != 'tumbleweed'
|
if: matrix.opensuse_version == 'leap:15.6'
|
||||||
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
|
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: >
|
run: >
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
sparsehash-devel
|
sparsehash-devel
|
||||||
rapidjson-devel
|
rapidjson-devel
|
||||||
- name: Install kdsingleapplication-qt6-devel
|
- name: Install kdsingleapplication-qt6-devel
|
||||||
if: matrix.opensuse_version == 'tumbleweed'
|
if: matrix.opensuse_version != 'leap:15.6'
|
||||||
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
|
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
|
||||||
- name: Build and install KDSingleApplication
|
- name: Build and install KDSingleApplication
|
||||||
if: matrix.opensuse_version == 'leap:15.6'
|
if: matrix.opensuse_version == 'leap:15.6'
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
cmake --build build --config Release --parallel 4
|
cmake --build build --config Release --parallel 4
|
||||||
cmake --install build
|
cmake --install build
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -115,14 +115,14 @@ jobs:
|
|||||||
- name: Copy source tarball
|
- name: Copy source tarball
|
||||||
working-directory: build
|
working-directory: build
|
||||||
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
|
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
|
||||||
- name: Build RPM (Tumbleweed)
|
- name: Build RPM
|
||||||
if: matrix.opensuse_version == 'tumbleweed'
|
if: matrix.opensuse_version != 'leap:15.6'
|
||||||
env:
|
env:
|
||||||
RPM_BUILD_NCPUS: 4
|
RPM_BUILD_NCPUS: 4
|
||||||
working-directory: build
|
working-directory: build
|
||||||
run: rpmbuild -ba strawberry.spec
|
run: rpmbuild -ba strawberry.spec
|
||||||
- name: Build RPM (Leap)
|
- name: Build RPM (leap:15.6)
|
||||||
if: matrix.opensuse_version != 'tumbleweed'
|
if: matrix.opensuse_version == 'leap:15.6'
|
||||||
env:
|
env:
|
||||||
RPM_BUILD_NCPUS: 4
|
RPM_BUILD_NCPUS: 4
|
||||||
CC: gcc-14
|
CC: gcc-14
|
||||||
@@ -134,14 +134,14 @@ jobs:
|
|||||||
run: echo "subdir=$(echo ${{matrix.opensuse_version}} | sed 's/leap:/lp/g' | sed 's/\.//g')" > $GITHUB_OUTPUT
|
run: echo "subdir=$(echo ${{matrix.opensuse_version}} | sed 's/leap:/lp/g' | sed 's/\.//g')" > $GITHUB_OUTPUT
|
||||||
- name: Upload source
|
- name: Upload source
|
||||||
if: matrix.opensuse_version == 'tumbleweed'
|
if: matrix.opensuse_version == 'tumbleweed'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: source
|
name: source
|
||||||
path: |
|
path: |
|
||||||
/usr/src/packages/SOURCES/*.xz
|
/usr/src/packages/SOURCES/*.xz
|
||||||
- name: Upload rpm
|
- name: Upload rpm
|
||||||
if: matrix.opensuse_version != 'tumbleweed'
|
if: matrix.opensuse_version != 'tumbleweed'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: opensuse-${{steps.set-subdir.outputs.subdir}}
|
name: opensuse-${{steps.set-subdir.outputs.subdir}}
|
||||||
path: |
|
path: |
|
||||||
@@ -156,7 +156,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
fedora_version: [ '39', '40', '41', '42' ]
|
fedora_version: [ '42', '43', '44' ]
|
||||||
container:
|
container:
|
||||||
image: fedora:${{matrix.fedora_version}}
|
image: fedora:${{matrix.fedora_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -209,7 +209,7 @@ jobs:
|
|||||||
sparsehash-devel
|
sparsehash-devel
|
||||||
rapidjson-devel
|
rapidjson-devel
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -234,7 +234,7 @@ jobs:
|
|||||||
working-directory: build
|
working-directory: build
|
||||||
run: rpmbuild -ba strawberry.spec
|
run: rpmbuild -ba strawberry.spec
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: fedora-${{matrix.fedora_version}}
|
name: fedora-${{matrix.fedora_version}}
|
||||||
path: |
|
path: |
|
||||||
@@ -307,7 +307,7 @@ jobs:
|
|||||||
- name: Remove files
|
- name: Remove files
|
||||||
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
|
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -333,7 +333,7 @@ jobs:
|
|||||||
run: rpmbuild -ba strawberry.spec
|
run: rpmbuild -ba strawberry.spec
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
if: matrix.openmandriva_version != 'cooker'
|
if: matrix.openmandriva_version != 'cooker'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: openmandriva-${{matrix.openmandriva_version}}
|
name: openmandriva-${{matrix.openmandriva_version}}
|
||||||
path: |
|
path: |
|
||||||
@@ -409,7 +409,7 @@ jobs:
|
|||||||
cmake --build build --config Release --parallel 4
|
cmake --build build --config Release --parallel 4
|
||||||
cmake --install build
|
cmake --install build
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -434,7 +434,7 @@ jobs:
|
|||||||
working-directory: build
|
working-directory: build
|
||||||
run: rpmbuild -ba strawberry.spec
|
run: rpmbuild -ba strawberry.spec
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: mageia-${{matrix.mageia_version}}
|
name: mageia-${{matrix.mageia_version}}
|
||||||
path: |
|
path: |
|
||||||
@@ -449,7 +449,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
debian_version: [ 'bookworm', 'trixie' ]
|
debian_version: [ 'bookworm', 'trixie', 'forky' ]
|
||||||
container:
|
container:
|
||||||
image: debian:${{matrix.debian_version}}
|
image: debian:${{matrix.debian_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -499,7 +499,11 @@ jobs:
|
|||||||
qt6-tools-dev-tools
|
qt6-tools-dev-tools
|
||||||
qt6-l10n-tools
|
qt6-l10n-tools
|
||||||
rapidjson-dev
|
rapidjson-dev
|
||||||
|
- name: Install KDSingleApplication
|
||||||
|
if: matrix.debian_version != 'bookworm'
|
||||||
|
run: apt install -y libkdsingleapplication-qt6-dev
|
||||||
- name: Build and install KDSingleApplication
|
- name: Build and install KDSingleApplication
|
||||||
|
if: matrix.debian_version == 'bookworm'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||||
cd KDSingleApplication
|
cd KDSingleApplication
|
||||||
@@ -507,7 +511,7 @@ jobs:
|
|||||||
cmake --build build --config Release --parallel 4
|
cmake --build build --config Release --parallel 4
|
||||||
cmake --install build
|
cmake --install build
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -524,7 +528,7 @@ jobs:
|
|||||||
- name: Copy deb
|
- name: Copy deb
|
||||||
run: cp ../*.deb .
|
run: cp ../*.deb .
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: debian-${{matrix.debian_version}}
|
name: debian-${{matrix.debian_version}}
|
||||||
path: |
|
path: |
|
||||||
@@ -538,7 +542,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
|
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
||||||
container:
|
container:
|
||||||
image: ubuntu:${{matrix.ubuntu_version}}
|
image: ubuntu:${{matrix.ubuntu_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -591,7 +595,11 @@ jobs:
|
|||||||
qt6-tools-dev-tools
|
qt6-tools-dev-tools
|
||||||
qt6-l10n-tools
|
qt6-l10n-tools
|
||||||
rapidjson-dev
|
rapidjson-dev
|
||||||
|
- name: Install KDSingleApplication
|
||||||
|
if: matrix.ubuntu_version != 'noble'
|
||||||
|
run: apt install -y libkdsingleapplication-qt6-dev
|
||||||
- name: Build and install KDSingleApplication
|
- name: Build and install KDSingleApplication
|
||||||
|
if: matrix.ubuntu_version == 'noble'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||||
cd KDSingleApplication
|
cd KDSingleApplication
|
||||||
@@ -599,7 +607,7 @@ jobs:
|
|||||||
cmake --build build --config Release --parallel 4
|
cmake --build build --config Release --parallel 4
|
||||||
cmake --install build
|
cmake --install build
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -616,7 +624,7 @@ jobs:
|
|||||||
- name: Copy deb
|
- name: Copy deb
|
||||||
run: cp ../*.deb ../*.ddeb .
|
run: cp ../*.deb ../*.ddeb .
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: ubuntu-${{matrix.ubuntu_version}}
|
name: ubuntu-${{matrix.ubuntu_version}}
|
||||||
path: |
|
path: |
|
||||||
@@ -631,7 +639,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
|
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
||||||
container:
|
container:
|
||||||
image: ubuntu:${{matrix.ubuntu_version}}
|
image: ubuntu:${{matrix.ubuntu_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -691,7 +699,7 @@ jobs:
|
|||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
run: apt install -y keyboxd
|
run: apt install -y keyboxd
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -727,16 +735,22 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
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
|
- name: Build FreeBSD
|
||||||
id: build-freebsd
|
id: build-freebsd
|
||||||
uses: vmactions/freebsd-vm@v1.2.0
|
uses: vmactions/freebsd-vm@v1.3.7
|
||||||
with:
|
with:
|
||||||
usesh: true
|
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
|
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: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
@@ -752,13 +766,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Build OpenBSD
|
- name: Build OpenBSD
|
||||||
id: build-openbsd
|
id: build-openbsd
|
||||||
uses: vmactions/openbsd-vm@v1.1.7
|
uses: vmactions/openbsd-vm@v1.3.1
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
mem: 4096
|
mem: 4096
|
||||||
@@ -779,7 +793,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
runner: [ 'macos-13', 'macos-15' ]
|
runner: [ 'macos-15-intel', 'macos-15' ]
|
||||||
buildtype: [ 'release' ]
|
buildtype: [ 'release' ]
|
||||||
|
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
@@ -788,7 +802,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set MACOSX_DEPLOYMENT_TARGET
|
- name: Set MACOSX_DEPLOYMENT_TARGET
|
||||||
run: |
|
run: |
|
||||||
for i in 13 14 15; do
|
for i in 12 13 14 15; do
|
||||||
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
|
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
|
||||||
echo "Using macOS SDK ${i}"
|
echo "Using macOS SDK ${i}"
|
||||||
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
|
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
|
||||||
@@ -818,14 +832,14 @@ jobs:
|
|||||||
rm -f uninstall.sh
|
rm -f uninstall.sh
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Import certificate file
|
- name: Import certificate file
|
||||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false
|
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:
|
with:
|
||||||
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE }}
|
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE }}
|
||||||
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
|
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
|
||||||
@@ -882,9 +896,9 @@ jobs:
|
|||||||
run: make deploy
|
run: make deploy
|
||||||
|
|
||||||
- name: Manually Codesign
|
- 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
|
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
|
- name: Manually Codesign
|
||||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
|
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
|
||||||
@@ -946,7 +960,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set MACOSX_DEPLOYMENT_TARGET
|
- name: Set MACOSX_DEPLOYMENT_TARGET
|
||||||
run: |
|
run: |
|
||||||
for i in 13 14 15; do
|
for i in 12 13 14 15; do
|
||||||
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
|
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
|
||||||
echo "Using macOS SDK ${i}"
|
echo "Using macOS SDK ${i}"
|
||||||
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
|
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
|
||||||
@@ -969,7 +983,7 @@ jobs:
|
|||||||
run: echo "cmake_buildtype=$(echo ${{env.buildtype}} | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}')" >> $GITHUB_ENV
|
run: echo "cmake_buildtype=$(echo ${{env.buildtype}} | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}')" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -1072,7 +1086,7 @@ jobs:
|
|||||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -1246,12 +1260,42 @@ jobs:
|
|||||||
build-windows-msvc:
|
build-windows-msvc:
|
||||||
name: Build Windows MSVC
|
name: Build Windows MSVC
|
||||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
||||||
runs-on: windows-2022
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ 'x86', 'x86_64' ]
|
include:
|
||||||
buildtype: [ 'release' ]
|
- name: "x86_64 debug"
|
||||||
|
runner: windows-2022
|
||||||
|
arch: x86_64
|
||||||
|
buildtype: debug
|
||||||
|
|
||||||
|
- name: "x86_64 release"
|
||||||
|
runner: windows-2022
|
||||||
|
arch: x86_64
|
||||||
|
buildtype: release
|
||||||
|
|
||||||
|
- name: "x86 debug"
|
||||||
|
runner: windows-2022
|
||||||
|
arch: x86
|
||||||
|
buildtype: debug
|
||||||
|
|
||||||
|
- name: "x86 release"
|
||||||
|
runner: windows-2022
|
||||||
|
arch: x86
|
||||||
|
buildtype: release
|
||||||
|
|
||||||
|
- name: "arm64 debug"
|
||||||
|
runner: windows-11-arm
|
||||||
|
arch: arm64
|
||||||
|
buildtype: debug
|
||||||
|
|
||||||
|
- name: "arm64 release"
|
||||||
|
runner: windows-11-arm
|
||||||
|
arch: arm64
|
||||||
|
buildtype: release
|
||||||
|
|
||||||
|
runs-on: ${{matrix.runner}}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Set prefix path
|
- name: Set prefix path
|
||||||
@@ -1265,6 +1309,20 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Show SDK versions
|
||||||
|
shell: bash
|
||||||
|
run: ls -la "c:/Program Files (x86)/Windows Kits/10/include"
|
||||||
|
|
||||||
|
- name: Set SDK version
|
||||||
|
if: matrix.arch != 'arm64'
|
||||||
|
shell: bash
|
||||||
|
run: echo "sdk_version=10.0.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
|
- name: Install rsync
|
||||||
shell: cmd
|
shell: cmd
|
||||||
run: choco install --no-progress rsync
|
run: choco install --no-progress rsync
|
||||||
@@ -1293,7 +1351,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Copy bin files
|
- name: Copy bin files
|
||||||
shell: bash
|
shell: bash
|
||||||
run: cp /c/strawberry/c/bin/{patch.exe,strip.exe,strings.exe,objdump.exe} ${{env.prefix_path_unix}}/bin
|
run: |
|
||||||
|
cp /c/mingw64/bin/{strip.exe,strings.exe,objdump.exe} ${{env.prefix_path_unix}}/bin
|
||||||
|
cp /c/strawberry/c/bin/patch.exe ${{env.prefix_path_unix}}/bin
|
||||||
|
|
||||||
- name: Delete conflicting files
|
- name: Delete conflicting files
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -1347,11 +1407,11 @@ jobs:
|
|||||||
uses: ilammy/msvc-dev-cmd@v1
|
uses: ilammy/msvc-dev-cmd@v1
|
||||||
with:
|
with:
|
||||||
arch: ${{matrix.arch}}
|
arch: ${{matrix.arch}}
|
||||||
sdk: 10.0.20348.0
|
sdk: ${{env.sdk_version}}
|
||||||
vsversion: 2022
|
vsversion: 2022
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -1364,15 +1424,18 @@ jobs:
|
|||||||
shell: cmd
|
shell: cmd
|
||||||
run: cmake -E make_directory build
|
run: cmake -E make_directory build
|
||||||
|
|
||||||
- name: Set ENABLE_WIN32_CONSOLE (debug)
|
- name: Set ENABLE_WIN32_CONSOLE
|
||||||
if: matrix.buildtype == 'debug'
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: echo "win32_console=ON" >> $GITHUB_ENV
|
run: echo "enable_win32_console=$(test "${{matrix.buildtype}}" = "debug" && echo "ON" || echo "OFF")" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set ENABLE_WIN32_CONSOLE (release)
|
- name: Set ENABLE_SPOTIFY
|
||||||
if: matrix.buildtype == 'release'
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: echo "win32_console=OFF" >> $GITHUB_ENV
|
run: echo "enable_spotify=$(test -f "${{env.prefix_path_unix}}/lib/gstreamer-1.0/gstspotify.dll" && echo "ON" || echo "OFF")" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Remove -lm from .pc files
|
||||||
|
if: matrix.arch == 'arm64'
|
||||||
|
shell: bash
|
||||||
|
run: sed -i 's/\-lm$//g' ${{env.prefix_path_unix}}/lib/pkgconfig/*.pc
|
||||||
|
|
||||||
- name: Run CMake
|
- name: Run CMake
|
||||||
shell: cmd
|
shell: cmd
|
||||||
@@ -1384,14 +1447,14 @@ jobs:
|
|||||||
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
|
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
|
||||||
-DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake"
|
-DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake"
|
||||||
-DARCH="${{matrix.arch}}"
|
-DARCH="${{matrix.arch}}"
|
||||||
-DENABLE_WIN32_CONSOLE=${{env.win32_console}}
|
-DENABLE_WIN32_CONSOLE=${{env.enable_win32_console}}
|
||||||
-DPKG_CONFIG_EXECUTABLE="${{env.prefix_path_forwardslash}}/bin/pkg-config.exe"
|
-DPKG_CONFIG_EXECUTABLE="${{env.prefix_path_forwardslash}}/bin/pkg-config.exe"
|
||||||
-DICU_ROOT="${{env.prefix_path_forwardslash}}"
|
-DICU_ROOT="${{env.prefix_path_forwardslash}}"
|
||||||
-DENABLE_GIO=OFF
|
-DENABLE_GIO=OFF
|
||||||
-DENABLE_AUDIOCD=OFF
|
-DENABLE_AUDIOCD=OFF
|
||||||
-DENABLE_MTP=OFF
|
-DENABLE_MTP=OFF
|
||||||
-DENABLE_GPOD=OFF
|
-DENABLE_GPOD=OFF
|
||||||
-DENABLE_SPOTIFY=ON
|
-DENABLE_SPOTIFY=${{env.enable_spotify}}
|
||||||
|
|
||||||
- name: Run Make
|
- name: Run Make
|
||||||
shell: cmd
|
shell: cmd
|
||||||
@@ -1581,11 +1644,11 @@ jobs:
|
|||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
run: sudo apt install -y git rsync
|
run: sudo apt install -y git rsync
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: SSH Setup
|
- name: SSH Setup
|
||||||
@@ -1629,7 +1692,7 @@ jobs:
|
|||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
run: sudo apt install -y git jq gh
|
run: sudo apt install -y git jq gh
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Show release assets
|
- name: Show release assets
|
||||||
@@ -1637,7 +1700,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||||
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
|
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: Add artifacts to release
|
- name: Add artifacts to release
|
||||||
|
|||||||
33
.gitignore
vendored
33
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
/build
|
|
||||||
/bin
|
/bin
|
||||||
/CMakeLists.txt.user
|
/CMakeLists.txt.user
|
||||||
|
/.qtcreator
|
||||||
/.kdev4
|
/.kdev4
|
||||||
/strawberry.kdev4
|
/strawberry.kdev4
|
||||||
/.vscode
|
/.vscode
|
||||||
@@ -12,3 +12,34 @@
|
|||||||
/CMakeSettings.json
|
/CMakeSettings.json
|
||||||
/dist/scripts/maketarball.sh
|
/dist/scripts/maketarball.sh
|
||||||
/debian/changelog
|
/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/**
|
||||||
|
|||||||
13
3rdparty/discord-rpc/CMakeLists.txt
vendored
13
3rdparty/discord-rpc/CMakeLists.txt
vendored
@@ -33,9 +33,20 @@ if(APPLE)
|
|||||||
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# RapidJSON (as packaged by Homebrew and others) can trigger C++17 deprecation
|
||||||
|
# warnings (e.g. std::iterator) when compiled with AppleClang/libc++.
|
||||||
|
# Keep the suppression narrowly scoped to this 3rdparty target.
|
||||||
|
if(APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||||
|
target_compile_options(discord-rpc PRIVATE -Wno-deprecated-declarations)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||||
endif()
|
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})
|
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 {
|
struct BaseConnection {
|
||||||
static BaseConnection *Create();
|
static BaseConnection *Create();
|
||||||
static void Destroy(BaseConnection *&);
|
static void Destroy(BaseConnection*&);
|
||||||
bool isOpen = false;
|
bool isOpen = false;
|
||||||
bool Open();
|
bool Open();
|
||||||
bool Close();
|
bool Close();
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ int GetProcessId() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct BaseConnectionUnix : public BaseConnection {
|
struct BaseConnectionUnix : public BaseConnection {
|
||||||
int sock { -1 };
|
int sock{ -1 };
|
||||||
};
|
};
|
||||||
|
|
||||||
static BaseConnectionUnix Connection;
|
static BaseConnectionUnix Connection;
|
||||||
static sockaddr_un PipeAddr {};
|
static sockaddr_un PipeAddr{};
|
||||||
#ifdef MSG_NOSIGNAL
|
#ifdef MSG_NOSIGNAL
|
||||||
static int MsgFlags = MSG_NOSIGNAL;
|
static int MsgFlags = MSG_NOSIGNAL;
|
||||||
#else
|
#else
|
||||||
@@ -105,7 +105,7 @@ bool BaseConnection::Open() {
|
|||||||
|
|
||||||
bool BaseConnection::Close() {
|
bool BaseConnection::Close() {
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||||
if (self->sock == -1) {
|
if (self->sock == -1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
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 {
|
struct BaseConnectionWin : public BaseConnection {
|
||||||
HANDLE pipe { INVALID_HANDLE_VALUE };
|
HANDLE pipe{ INVALID_HANDLE_VALUE };
|
||||||
};
|
};
|
||||||
|
|
||||||
static BaseConnectionWin Connection;
|
static BaseConnectionWin Connection;
|
||||||
@@ -57,10 +57,10 @@ void BaseConnection::Destroy(BaseConnection *&c) {
|
|||||||
|
|
||||||
bool BaseConnection::Open() {
|
bool BaseConnection::Open() {
|
||||||
|
|
||||||
wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" };
|
wchar_t pipeName[]{ L"\\\\?\\pipe\\discord-ipc-0" };
|
||||||
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
||||||
pipeName[pipeDigit] = L'0';
|
pipeName[pipeDigit] = L'0';
|
||||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||||
if (self->pipe != INVALID_HANDLE_VALUE) {
|
if (self->pipe != INVALID_HANDLE_VALUE) {
|
||||||
@@ -88,7 +88,7 @@ bool BaseConnection::Open() {
|
|||||||
|
|
||||||
bool BaseConnection::Close() {
|
bool BaseConnection::Close() {
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||||
::CloseHandle(self->pipe);
|
::CloseHandle(self->pipe);
|
||||||
self->pipe = INVALID_HANDLE_VALUE;
|
self->pipe = INVALID_HANDLE_VALUE;
|
||||||
self->isOpen = false;
|
self->isOpen = false;
|
||||||
@@ -102,7 +102,7 @@ bool BaseConnection::Write(const void *data, size_t length) {
|
|||||||
if (length == 0) {
|
if (length == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||||
assert(self);
|
assert(self);
|
||||||
if (!self) {
|
if (!self) {
|
||||||
return false;
|
return false;
|
||||||
@@ -127,7 +127,7 @@ bool BaseConnection::Read(void *data, size_t length) {
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||||
assert(self);
|
assert(self);
|
||||||
if (!self) {
|
if (!self) {
|
||||||
return false;
|
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>
|
template<typename ElementType, std::size_t QueueSize>
|
||||||
class MsgQueue {
|
class MsgQueue {
|
||||||
ElementType queue_[QueueSize];
|
ElementType queue_[QueueSize];
|
||||||
std::atomic_uint nextAdd_ { 0 };
|
std::atomic_uint nextAdd_{ 0 };
|
||||||
std::atomic_uint nextSend_ { 0 };
|
std::atomic_uint nextSend_{ 0 };
|
||||||
std::atomic_uint pendingSends_ { 0 };
|
std::atomic_uint pendingSends_{ 0 };
|
||||||
|
|
||||||
public:
|
public:
|
||||||
MsgQueue() {}
|
MsgQueue() {}
|
||||||
|
|||||||
@@ -163,4 +163,3 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
|
|||||||
Discord_RegisterW(appId, wcommand);
|
Discord_RegisterW(appId, wcommand);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
154
3rdparty/discord-rpc/discord_rpc.cpp
vendored
154
3rdparty/discord-rpc/discord_rpc.cpp
vendored
@@ -34,11 +34,15 @@
|
|||||||
#include "discord_rpc_connection.h"
|
#include "discord_rpc_connection.h"
|
||||||
#include "discord_serialization.h"
|
#include "discord_serialization.h"
|
||||||
|
|
||||||
namespace discord_rpc {
|
using namespace discord_rpc;
|
||||||
|
|
||||||
constexpr size_t MaxMessageSize { 16 * 1024 };
|
static void Discord_UpdateConnection();
|
||||||
constexpr size_t MessageQueueSize { 8 };
|
|
||||||
constexpr size_t JoinQueueSize { 8 };
|
namespace {
|
||||||
|
|
||||||
|
constexpr size_t MaxMessageSize{ 16 * 1024 };
|
||||||
|
constexpr size_t MessageQueueSize{ 8 };
|
||||||
|
constexpr size_t JoinQueueSize{ 8 };
|
||||||
|
|
||||||
struct QueuedMessage {
|
struct QueuedMessage {
|
||||||
size_t length;
|
size_t length;
|
||||||
@@ -66,24 +70,24 @@ struct User {
|
|||||||
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
|
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
|
||||||
};
|
};
|
||||||
|
|
||||||
static RpcConnection *Connection { nullptr };
|
static RpcConnection *Connection{ nullptr };
|
||||||
static DiscordEventHandlers QueuedHandlers {};
|
static DiscordEventHandlers QueuedHandlers{};
|
||||||
static DiscordEventHandlers Handlers {};
|
static DiscordEventHandlers Handlers{};
|
||||||
static std::atomic_bool WasJustConnected { false };
|
static std::atomic_bool WasJustConnected{ false };
|
||||||
static std::atomic_bool WasJustDisconnected { false };
|
static std::atomic_bool WasJustDisconnected{ false };
|
||||||
static std::atomic_bool GotErrorMessage { false };
|
static std::atomic_bool GotErrorMessage{ false };
|
||||||
static std::atomic_bool WasJoinGame { false };
|
static std::atomic_bool WasJoinGame{ false };
|
||||||
static std::atomic_bool WasSpectateGame { false };
|
static std::atomic_bool WasSpectateGame{ false };
|
||||||
static std::atomic_bool UpdatePresence { false };
|
static std::atomic_bool UpdatePresence{ false };
|
||||||
static char JoinGameSecret[256];
|
static char JoinGameSecret[256];
|
||||||
static char SpectateGameSecret[256];
|
static char SpectateGameSecret[256];
|
||||||
static int LastErrorCode { 0 };
|
static int LastErrorCode{ 0 };
|
||||||
static char LastErrorMessage[256];
|
static char LastErrorMessage[256];
|
||||||
static int LastDisconnectErrorCode { 0 };
|
static int LastDisconnectErrorCode{ 0 };
|
||||||
static char LastDisconnectErrorMessage[256];
|
static char LastDisconnectErrorMessage[256];
|
||||||
static std::mutex PresenceMutex;
|
static std::mutex PresenceMutex;
|
||||||
static std::mutex HandlerMutex;
|
static std::mutex HandlerMutex;
|
||||||
static QueuedMessage QueuedPresence {};
|
static QueuedMessage QueuedPresence{};
|
||||||
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
||||||
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
||||||
static User connectedUser;
|
static User connectedUser;
|
||||||
@@ -91,13 +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
|
// 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 Backoff ReconnectTimeMs(500, 60 * 1000);
|
||||||
static auto NextConnect = std::chrono::system_clock::now();
|
static auto NextConnect = std::chrono::system_clock::now();
|
||||||
static int Pid { 0 };
|
static int Pid{ 0 };
|
||||||
static int Nonce { 1 };
|
static int Nonce{ 1 };
|
||||||
|
|
||||||
static void Discord_UpdateConnection(void);
|
|
||||||
class IoThreadHolder {
|
class IoThreadHolder {
|
||||||
private:
|
private:
|
||||||
std::atomic_bool keepRunning { true };
|
std::atomic_bool keepRunning{ true };
|
||||||
std::mutex waitForIOMutex;
|
std::mutex waitForIOMutex;
|
||||||
std::condition_variable waitForIOActivity;
|
std::condition_variable waitForIOActivity;
|
||||||
std::thread ioThread;
|
std::thread ioThread;
|
||||||
@@ -106,14 +109,14 @@ class IoThreadHolder {
|
|||||||
void Start() {
|
void Start() {
|
||||||
keepRunning.store(true);
|
keepRunning.store(true);
|
||||||
ioThread = std::thread([&]() {
|
ioThread = std::thread([&]() {
|
||||||
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
|
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();
|
Discord_UpdateConnection();
|
||||||
}
|
while (keepRunning.load()) {
|
||||||
});
|
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
||||||
|
waitForIOActivity.wait_for(lock, maxWait);
|
||||||
|
Discord_UpdateConnection();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Notify() { waitForIOActivity.notify_all(); }
|
void Notify() { waitForIOActivity.notify_all(); }
|
||||||
@@ -128,7 +131,8 @@ class IoThreadHolder {
|
|||||||
|
|
||||||
~IoThreadHolder() { Stop(); }
|
~IoThreadHolder() { Stop(); }
|
||||||
};
|
};
|
||||||
static IoThreadHolder *IoThread { nullptr };
|
|
||||||
|
static IoThreadHolder *IoThread{ nullptr };
|
||||||
|
|
||||||
static void UpdateReconnectTime() {
|
static void UpdateReconnectTime() {
|
||||||
|
|
||||||
@@ -136,6 +140,44 @@ static void UpdateReconnectTime() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void SignalIOActivity() {
|
||||||
|
|
||||||
|
if (IoThread != nullptr) {
|
||||||
|
IoThread->Notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool RegisterForEvent(const char *evtName) {
|
||||||
|
|
||||||
|
auto qmessage = SendQueue.GetNextAddMessage();
|
||||||
|
if (qmessage) {
|
||||||
|
qmessage->length = JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||||
|
SendQueue.CommitAdd();
|
||||||
|
SignalIOActivity();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool DeregisterForEvent(const char *evtName) {
|
||||||
|
|
||||||
|
auto qmessage = SendQueue.GetNextAddMessage();
|
||||||
|
if (qmessage) {
|
||||||
|
qmessage->length = JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||||
|
SendQueue.CommitAdd();
|
||||||
|
SignalIOActivity();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
static void Discord_UpdateConnection() {
|
static void Discord_UpdateConnection() {
|
||||||
|
|
||||||
if (!Connection) {
|
if (!Connection) {
|
||||||
@@ -242,42 +284,6 @@ static void Discord_UpdateConnection() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SignalIOActivity() {
|
|
||||||
|
|
||||||
if (IoThread != nullptr) {
|
|
||||||
IoThread->Notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool RegisterForEvent(const char *evtName) {
|
|
||||||
|
|
||||||
auto qmessage = SendQueue.GetNextAddMessage();
|
|
||||||
if (qmessage) {
|
|
||||||
qmessage->length = JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
|
||||||
SendQueue.CommitAdd();
|
|
||||||
SignalIOActivity();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool DeregisterForEvent(const char *evtName) {
|
|
||||||
|
|
||||||
auto qmessage = SendQueue.GetNextAddMessage();
|
|
||||||
if (qmessage) {
|
|
||||||
qmessage->length = JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
|
||||||
SendQueue.CommitAdd();
|
|
||||||
SignalIOActivity();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
|
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
|
||||||
|
|
||||||
IoThread = new (std::nothrow) IoThreadHolder();
|
IoThread = new (std::nothrow) IoThreadHolder();
|
||||||
@@ -348,7 +354,7 @@ extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandle
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" void Discord_Shutdown(void) {
|
extern "C" void Discord_Shutdown() {
|
||||||
|
|
||||||
if (!Connection) {
|
if (!Connection) {
|
||||||
return;
|
return;
|
||||||
@@ -423,7 +429,7 @@ extern "C" void Discord_RunCallbacks() {
|
|||||||
if (WasJustConnected.exchange(false)) {
|
if (WasJustConnected.exchange(false)) {
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||||
if (Handlers.ready) {
|
if (Handlers.ready) {
|
||||||
DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
DiscordUser du{ connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||||
Handlers.ready(&du);
|
Handlers.ready(&du);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,7 +465,7 @@ extern "C" void Discord_RunCallbacks() {
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||||
if (Handlers.joinRequest) {
|
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);
|
Handlers.joinRequest(&du);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -480,12 +486,12 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
|||||||
|
|
||||||
if (newHandlers) {
|
if (newHandlers) {
|
||||||
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
||||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||||
RegisterForEvent(event); \
|
RegisterForEvent(event); \
|
||||||
} \
|
} \
|
||||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
||||||
DeregisterForEvent(event); \
|
DeregisterForEvent(event); \
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||||
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
|
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
|
||||||
@@ -502,5 +508,3 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
|
|||||||
5
3rdparty/discord-rpc/discord_rpc.h
vendored
5
3rdparty/discord-rpc/discord_rpc.h
vendored
@@ -26,14 +26,13 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace discord_rpc {
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
typedef struct DiscordRichPresence {
|
typedef struct DiscordRichPresence {
|
||||||
int type;
|
int type;
|
||||||
|
int status_display_type;
|
||||||
const char *name; /* max 128 bytes */
|
const char *name; /* max 128 bytes */
|
||||||
const char *state; /* max 128 bytes */
|
const char *state; /* max 128 bytes */
|
||||||
const char *details; /* max 128 bytes */
|
const char *details; /* max 128 bytes */
|
||||||
@@ -92,6 +91,4 @@ void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
|
|||||||
} /* extern "C" */
|
} /* extern "C" */
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
|
|
||||||
#endif // DISCORD_RPC_H
|
#endif // DISCORD_RPC_H
|
||||||
|
|||||||
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,
|
Connected,
|
||||||
};
|
};
|
||||||
|
|
||||||
BaseConnection *connection { nullptr };
|
BaseConnection *connection{ nullptr };
|
||||||
State state { State::Disconnected };
|
State state{ State::Disconnected };
|
||||||
void (*onConnect)(JsonDocument &message) { nullptr };
|
void (*onConnect)(JsonDocument &message) { nullptr };
|
||||||
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
|
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
|
||||||
char appId[64] {};
|
char appId[64]{};
|
||||||
int lastErrorCode { 0 };
|
int lastErrorCode{ 0 };
|
||||||
char lastErrorMessage[256] {};
|
char lastErrorMessage[256]{};
|
||||||
RpcConnection::MessageFrame sendFrame;
|
RpcConnection::MessageFrame sendFrame;
|
||||||
|
|
||||||
static RpcConnection *Create(const char *applicationId);
|
static RpcConnection *Create(const char *applicationId);
|
||||||
static void Destroy(RpcConnection *&);
|
static void Destroy(RpcConnection*&);
|
||||||
|
|
||||||
inline bool IsOpen() const { return state == State::Connected; }
|
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) {
|
if (presence->type >= 0 && presence->type <= 5) {
|
||||||
WriteKey(writer, "type");
|
WriteKey(writer, "type");
|
||||||
writer.Int(presence->type);
|
writer.Int(presence->type);
|
||||||
|
|
||||||
|
WriteKey(writer, "status_display_type");
|
||||||
|
writer.Int(presence->status_display_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteOptionalString(writer, "name", presence->name);
|
WriteOptionalString(writer, "name", presence->name);
|
||||||
|
|||||||
3
3rdparty/discord-rpc/discord_serialization.h
vendored
3
3rdparty/discord-rpc/discord_serialization.h
vendored
@@ -28,6 +28,8 @@
|
|||||||
#include <rapidjson/stringbuffer.h>
|
#include <rapidjson/stringbuffer.h>
|
||||||
#include <rapidjson/writer.h>
|
#include <rapidjson/writer.h>
|
||||||
|
|
||||||
|
struct DiscordRichPresence;
|
||||||
|
|
||||||
namespace discord_rpc {
|
namespace discord_rpc {
|
||||||
|
|
||||||
// if only there was a standard library function for this
|
// if only there was a standard library function for this
|
||||||
@@ -48,7 +50,6 @@ inline size_t StringCopy(char (&dest)[Len], const char *src) {
|
|||||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
|
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
|
||||||
|
|
||||||
// Commands
|
// Commands
|
||||||
struct DiscordRichPresence;
|
|
||||||
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
|
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
|
||||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
||||||
|
|
||||||
|
|||||||
62
Brewfile
Normal file
62
Brewfile
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# 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; we tap *this repo* via file://.
|
||||||
|
# Use the Brewfile's directory (repo root) rather than the current working directory,
|
||||||
|
# so `brew bundle --file /path/to/Brewfile` works no matter where you run it from.
|
||||||
|
tap "strawberry/local", "file://#{File.expand_path(__dir__)}"
|
||||||
|
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"
|
||||||
|
|
||||||
104
CMakeLists.txt
104
CMakeLists.txt
@@ -6,6 +6,14 @@ if(APPLE)
|
|||||||
enable_language(OBJC OBJCXX)
|
enable_language(OBJC OBJCXX)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(APPLE)
|
||||||
|
option(BUILD_FOR_MAC_APP_STORE "Build for Mac App Store (MAS): disables Sparkle + any localhost port-listener OAuth redirect server, and uses MAS-focused defaults." OFF)
|
||||||
|
else()
|
||||||
|
set(BUILD_FOR_MAC_APP_STORE OFF)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(MACOS_BUNDLE_ID "com.dryark.strawberry" CACHE STRING "macOS bundle identifier (CFBundleIdentifier)")
|
||||||
|
|
||||||
if(POLICY CMP0054)
|
if(POLICY CMP0054)
|
||||||
cmake_policy(SET CMP0054 NEW)
|
cmake_policy(SET CMP0054 NEW)
|
||||||
endif()
|
endif()
|
||||||
@@ -32,6 +40,24 @@ if(LINUX)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
|
if(BUILD_FOR_MAC_APP_STORE)
|
||||||
|
# MAS builds: Sparkle (and QtSparkle) must be disabled.
|
||||||
|
set(ENABLE_SPARKLE OFF CACHE BOOL "Sparkle integration" FORCE)
|
||||||
|
set(ENABLE_QTSPARKLE OFF CACHE BOOL "QtSparkle integration" FORCE)
|
||||||
|
else()
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
endif()
|
||||||
include(cmake/Dmg.cmake)
|
include(cmake/Dmg.cmake)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -84,8 +110,6 @@ if(MSVC)
|
|||||||
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
|
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
|
||||||
else()
|
else()
|
||||||
list(APPEND COMPILE_OPTIONS
|
list(APPEND COMPILE_OPTIONS
|
||||||
$<$<COMPILE_LANGUAGE:C>:-std=c11>
|
|
||||||
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
|
|
||||||
-Wall
|
-Wall
|
||||||
-Wextra
|
-Wextra
|
||||||
-Wpedantic
|
-Wpedantic
|
||||||
@@ -253,17 +277,23 @@ endif()
|
|||||||
|
|
||||||
find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
|
find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
|
||||||
|
|
||||||
if(APPLE)
|
|
||||||
find_library(SPARKLE Sparkle)
|
|
||||||
#find_package(SPMediaKeyTap REQUIRED)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
find_package(getopt-win REQUIRED)
|
find_package(getopt NAMES getopt getopt-win unofficial-getopt-win32 REQUIRED)
|
||||||
|
if(TARGET getopt::getopt)
|
||||||
|
set(GETOPT_LIBRARIES getopt::getopt)
|
||||||
|
elseif(TARGET getopt-win::getopt)
|
||||||
|
set(GETOPT_LIBRARIES getopt-win::getopt)
|
||||||
|
elseif(TARGET getopt::getopt_shared)
|
||||||
|
set(GETOPT_LIBRARIES getopt::getopt_shared)
|
||||||
|
elseif(TARGET unofficial::getopt-win32::getopt)
|
||||||
|
set(GETOPT_LIBRARIES unofficial::getopt-win32::getopt)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Missing getopt")
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(APPLE OR WIN32)
|
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")
|
if(TARGET "qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle")
|
||||||
set(QTSPARKLE_FOUND ON)
|
set(QTSPARKLE_FOUND ON)
|
||||||
endif()
|
endif()
|
||||||
@@ -690,6 +720,7 @@ set(SOURCES
|
|||||||
src/lyrics/elyricsnetlyricsprovider.cpp
|
src/lyrics/elyricsnetlyricsprovider.cpp
|
||||||
src/lyrics/letraslyricsprovider.cpp
|
src/lyrics/letraslyricsprovider.cpp
|
||||||
src/lyrics/lyricfindlyricsprovider.cpp
|
src/lyrics/lyricfindlyricsprovider.cpp
|
||||||
|
src/lyrics/lrcliblyricsprovider.cpp
|
||||||
|
|
||||||
src/settings/settingsdialog.cpp
|
src/settings/settingsdialog.cpp
|
||||||
src/settings/settingspage.cpp
|
src/settings/settingspage.cpp
|
||||||
@@ -783,9 +814,7 @@ set(SOURCES
|
|||||||
src/scrobbler/scrobblercache.cpp
|
src/scrobbler/scrobblercache.cpp
|
||||||
src/scrobbler/scrobblercacheitem.cpp
|
src/scrobbler/scrobblercacheitem.cpp
|
||||||
src/scrobbler/scrobblemetadata.cpp
|
src/scrobbler/scrobblemetadata.cpp
|
||||||
src/scrobbler/scrobblingapi20.cpp
|
|
||||||
src/scrobbler/lastfmscrobbler.cpp
|
src/scrobbler/lastfmscrobbler.cpp
|
||||||
src/scrobbler/librefmscrobbler.cpp
|
|
||||||
src/scrobbler/listenbrainzscrobbler.cpp
|
src/scrobbler/listenbrainzscrobbler.cpp
|
||||||
src/scrobbler/lastfmimport.cpp
|
src/scrobbler/lastfmimport.cpp
|
||||||
|
|
||||||
@@ -813,6 +842,8 @@ set(SOURCES
|
|||||||
|
|
||||||
src/fileview/fileview.cpp
|
src/fileview/fileview.cpp
|
||||||
src/fileview/fileviewlist.cpp
|
src/fileview/fileviewlist.cpp
|
||||||
|
src/fileview/fileviewtree.cpp
|
||||||
|
src/fileview/fileviewtreemodel.cpp
|
||||||
|
|
||||||
src/device/devicemanager.cpp
|
src/device/devicemanager.cpp
|
||||||
src/device/devicelister.cpp
|
src/device/devicelister.cpp
|
||||||
@@ -988,6 +1019,7 @@ set(HEADERS
|
|||||||
src/lyrics/elyricsnetlyricsprovider.h
|
src/lyrics/elyricsnetlyricsprovider.h
|
||||||
src/lyrics/letraslyricsprovider.h
|
src/lyrics/letraslyricsprovider.h
|
||||||
src/lyrics/lyricfindlyricsprovider.h
|
src/lyrics/lyricfindlyricsprovider.h
|
||||||
|
src/lyrics/lrcliblyricsprovider.h
|
||||||
|
|
||||||
src/settings/settingsdialog.h
|
src/settings/settingsdialog.h
|
||||||
src/settings/settingspage.h
|
src/settings/settingspage.h
|
||||||
@@ -1076,9 +1108,7 @@ set(HEADERS
|
|||||||
src/scrobbler/scrobblersettingsservice.h
|
src/scrobbler/scrobblersettingsservice.h
|
||||||
src/scrobbler/scrobblerservice.h
|
src/scrobbler/scrobblerservice.h
|
||||||
src/scrobbler/scrobblercache.h
|
src/scrobbler/scrobblercache.h
|
||||||
src/scrobbler/scrobblingapi20.h
|
|
||||||
src/scrobbler/lastfmscrobbler.h
|
src/scrobbler/lastfmscrobbler.h
|
||||||
src/scrobbler/librefmscrobbler.h
|
|
||||||
src/scrobbler/listenbrainzscrobbler.h
|
src/scrobbler/listenbrainzscrobbler.h
|
||||||
src/scrobbler/lastfmimport.h
|
src/scrobbler/lastfmimport.h
|
||||||
|
|
||||||
@@ -1103,6 +1133,8 @@ set(HEADERS
|
|||||||
|
|
||||||
src/fileview/fileview.h
|
src/fileview/fileview.h
|
||||||
src/fileview/fileviewlist.h
|
src/fileview/fileviewlist.h
|
||||||
|
src/fileview/fileviewtree.h
|
||||||
|
src/fileview/fileviewtreemodel.h
|
||||||
|
|
||||||
src/device/devicemanager.h
|
src/device/devicemanager.h
|
||||||
src/device/devicelister.h
|
src/device/devicelister.h
|
||||||
@@ -1205,6 +1237,10 @@ set(UI
|
|||||||
src/device/deviceviewcontainer.ui
|
src/device/deviceviewcontainer.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(UNIX)
|
||||||
|
optional_source(UNIX SOURCES src/core/unixsignalwatcher.cpp HEADERS src/core/unixsignalwatcher.h)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
optional_source(APPLE
|
optional_source(APPLE
|
||||||
SOURCES
|
SOURCES
|
||||||
@@ -1433,6 +1469,7 @@ optional_source(HAVE_SPOTIFY
|
|||||||
src/spotify/spotifybaserequest.cpp
|
src/spotify/spotifybaserequest.cpp
|
||||||
src/spotify/spotifyrequest.cpp
|
src/spotify/spotifyrequest.cpp
|
||||||
src/spotify/spotifyfavoriterequest.cpp
|
src/spotify/spotifyfavoriterequest.cpp
|
||||||
|
src/spotify/spotifymetadatarequest.cpp
|
||||||
src/settings/spotifysettingspage.cpp
|
src/settings/spotifysettingspage.cpp
|
||||||
src/covermanager/spotifycoverprovider.cpp
|
src/covermanager/spotifycoverprovider.cpp
|
||||||
HEADERS
|
HEADERS
|
||||||
@@ -1440,6 +1477,7 @@ optional_source(HAVE_SPOTIFY
|
|||||||
src/spotify/spotifybaserequest.h
|
src/spotify/spotifybaserequest.h
|
||||||
src/spotify/spotifyrequest.h
|
src/spotify/spotifyrequest.h
|
||||||
src/spotify/spotifyfavoriterequest.h
|
src/spotify/spotifyfavoriterequest.h
|
||||||
|
src/spotify/spotifymetadatarequest.h
|
||||||
src/settings/spotifysettingspage.h
|
src/settings/spotifysettingspage.h
|
||||||
src/covermanager/spotifycoverprovider.h
|
src/covermanager/spotifycoverprovider.h
|
||||||
UI
|
UI
|
||||||
@@ -1454,6 +1492,8 @@ optional_source(HAVE_QOBUZ
|
|||||||
src/qobuz/qobuzrequest.cpp
|
src/qobuz/qobuzrequest.cpp
|
||||||
src/qobuz/qobuzstreamurlrequest.cpp
|
src/qobuz/qobuzstreamurlrequest.cpp
|
||||||
src/qobuz/qobuzfavoriterequest.cpp
|
src/qobuz/qobuzfavoriterequest.cpp
|
||||||
|
src/qobuz/qobuzmetadatarequest.cpp
|
||||||
|
src/qobuz/qobuzcredentialfetcher.cpp
|
||||||
src/settings/qobuzsettingspage.cpp
|
src/settings/qobuzsettingspage.cpp
|
||||||
src/covermanager/qobuzcoverprovider.cpp
|
src/covermanager/qobuzcoverprovider.cpp
|
||||||
HEADERS
|
HEADERS
|
||||||
@@ -1463,6 +1503,8 @@ optional_source(HAVE_QOBUZ
|
|||||||
src/qobuz/qobuzrequest.h
|
src/qobuz/qobuzrequest.h
|
||||||
src/qobuz/qobuzstreamurlrequest.h
|
src/qobuz/qobuzstreamurlrequest.h
|
||||||
src/qobuz/qobuzfavoriterequest.h
|
src/qobuz/qobuzfavoriterequest.h
|
||||||
|
src/qobuz/qobuzmetadatarequest.h
|
||||||
|
src/qobuz/qobuzcredentialfetcher.h
|
||||||
src/settings/qobuzsettingspage.h
|
src/settings/qobuzsettingspage.h
|
||||||
src/covermanager/qobuzcoverprovider.h
|
src/covermanager/qobuzcoverprovider.h
|
||||||
UI
|
UI
|
||||||
@@ -1475,6 +1517,21 @@ qt_add_resources(SOURCES data/data.qrc data/icons.qrc)
|
|||||||
|
|
||||||
add_library(strawberry_lib STATIC ${SOURCES})
|
add_library(strawberry_lib STATIC ${SOURCES})
|
||||||
|
|
||||||
|
# Treat Boost headers as system headers to avoid noisy warnings from 3rdparty
|
||||||
|
# Boost code (e.g. -Wold-style-cast) when building Strawberry with strict flags.
|
||||||
|
set(_strawberry_boost_system_includes "")
|
||||||
|
if(TARGET Boost::headers)
|
||||||
|
get_target_property(_strawberry_boost_system_includes Boost::headers INTERFACE_INCLUDE_DIRECTORIES)
|
||||||
|
elseif(TARGET Boost::boost)
|
||||||
|
get_target_property(_strawberry_boost_system_includes Boost::boost INTERFACE_INCLUDE_DIRECTORIES)
|
||||||
|
elseif(DEFINED Boost_INCLUDE_DIRS)
|
||||||
|
set(_strawberry_boost_system_includes "${Boost_INCLUDE_DIRS}")
|
||||||
|
endif()
|
||||||
|
if(_strawberry_boost_system_includes)
|
||||||
|
target_include_directories(strawberry_lib SYSTEM PRIVATE ${_strawberry_boost_system_includes})
|
||||||
|
endif()
|
||||||
|
unset(_strawberry_boost_system_includes)
|
||||||
|
|
||||||
target_sources(strawberry PRIVATE src/main.cpp)
|
target_sources(strawberry PRIVATE src/main.cpp)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
@@ -1498,10 +1555,22 @@ if(HAVE_DISCORD_RPC)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(HAVE_TRANSLATIONS)
|
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)
|
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)
|
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")
|
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)
|
if(NOT INSTALL_TRANSLATIONS)
|
||||||
qt_add_resources(strawberry "translations" PREFIX "/i18n" BASE "${CMAKE_CURRENT_BINARY_DIR}/data" FILES "${INSTALL_TRANSLATIONS_FILES}")
|
qt_add_resources(strawberry "translations" PREFIX "/i18n" BASE "${CMAKE_CURRENT_BINARY_DIR}/data" FILES "${INSTALL_TRANSLATIONS_FILES}")
|
||||||
endif()
|
endif()
|
||||||
@@ -1554,9 +1623,10 @@ target_link_libraries(strawberry_lib PUBLIC
|
|||||||
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
|
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
|
||||||
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
|
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
|
||||||
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
|
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
|
||||||
$<$<BOOL:${WIN32}>:dsound dwmapi getopt-win::getopt>
|
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
|
||||||
$<$<BOOL:${MSVC}>:WindowsApp>
|
$<$<BOOL:${MSVC}>:WindowsApp>
|
||||||
KDAB::kdsingleapplication
|
KDAB::kdsingleapplication
|
||||||
|
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
|
||||||
)
|
)
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
@@ -1575,10 +1645,6 @@ if(APPLE)
|
|||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(HAVE_DISCORD_RPC)
|
|
||||||
target_link_libraries(strawberry_lib PRIVATE discord-rpc)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(strawberry PUBLIC strawberry_lib)
|
target_link_libraries(strawberry PUBLIC strawberry_lib)
|
||||||
|
|
||||||
if(NOT APPLE)
|
if(NOT APPLE)
|
||||||
|
|||||||
141
Changelog
141
Changelog
@@ -2,6 +2,145 @@ Strawberry Music Player
|
|||||||
=======================
|
=======================
|
||||||
ChangeLog
|
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.
|
||||||
|
* Fixed context album cover showing even when it's disabled in the setting (#1744).
|
||||||
|
* Fixed crash when dragging songs to a closed playlist (#1741).
|
||||||
|
* Enable startup notify in desktop file.
|
||||||
|
* (Windows|MSVC) Add experimental support for native ARM64 builds.
|
||||||
|
* (Windows|MinGW) Fixed crash on exit.
|
||||||
|
|
||||||
|
Version 1.2.10 (2025.04.18):
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
* Fixed Discord rich presence showing bogus artist and album.
|
||||||
|
* Fixed incorrect ID3v2 comment tag.
|
||||||
|
* (macOS|Windows MSVC) Fixed stuck playback of some streams.
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
* Removed Genius lyrics (longer working properly because of website changes).
|
||||||
|
* (macOS|Windows MSVC) Added back Spotify
|
||||||
|
|
||||||
Version 1.2.9 (2025.04.08):
|
Version 1.2.9 (2025.04.08):
|
||||||
|
|
||||||
Bugfixes:
|
Bugfixes:
|
||||||
@@ -204,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.
|
* 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.
|
* (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 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:
|
Enhancements:
|
||||||
* Improve error messages when connecting and copying to devices.
|
* 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.
|
||||||
|
|
||||||
22
Formula/macdeploycheck.rb
Normal file
22
Formula/macdeploycheck.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
class Macdeploycheck < Formula
|
||||||
|
desc "Sanity checks a macOS .app bundle for accidental Homebrew runtime dependencies"
|
||||||
|
homepage "https://github.com/strawberrymusicplayer/strawberry"
|
||||||
|
version "0.1.0"
|
||||||
|
# Homebrew requires a URL stanza. Use the script shipped in this tap (file://),
|
||||||
|
# so installs always match the tapped revision.
|
||||||
|
url "file://#{File.expand_path("../dist/macos/macdeploycheck.sh", __dir__)}"
|
||||||
|
sha256 "07d361dcecf98af44fa076cc4253af907e23dee273c198a60128dae41b98432d"
|
||||||
|
license "MIT"
|
||||||
|
|
||||||
|
depends_on :macos
|
||||||
|
|
||||||
|
def install
|
||||||
|
bin.install "macdeploycheck.sh" => "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`).
|
||||||
|
|
||||||
177
README.md
177
README.md
@@ -1,118 +1,127 @@
|
|||||||
:strawberry: Strawberry Music Player [](https://github.com/strawberrymusicplayer/strawberry/actions)
|
# Strawberry (macOS-focused fork)
|
||||||
=======================
|
|
||||||
[](https://github.com/sponsors/jonaski)
|
|
||||||
[](https://patreon.com/jonaskvinge)
|
|
||||||
[](https://paypal.me/jonaskvinge)
|
|
||||||
|
|
||||||
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt framework.
|
This repository is a **macOS-focused fork** of upstream Strawberry.
|
||||||
|
|
||||||

|
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/
|
## Upstream vs this fork (macOS distribution)
|
||||||
* Wiki: https://wiki.strawberrymusicplayer.org/
|
|
||||||
* Forum: https://forum.strawberrymusicplayer.org/
|
|
||||||
* Github: https://github.com/strawberrymusicplayer/strawberry
|
|
||||||
* Latest builds: https://builds.strawberrymusicplayer.org/
|
|
||||||
* openSUSE buildservice: https://build.opensuse.org/package/show/home:jonaski:audio/strawberry
|
|
||||||
* Ubuntu PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
|
||||||
* Ubuntu Unstable PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
|
|
||||||
* Translations: https://crowdin.com/project/strawberrymusicplayer/
|
|
||||||
|
|
||||||
### :bangbang: Opening an issue
|
Upstream Strawberry is where ongoing development happens:
|
||||||
|
|
||||||
* Read the FAQ: https://wiki.strawberrymusicplayer.org/wiki/FAQ
|
- Upstream: `https://github.com/strawberrymusicplayer/strawberry`
|
||||||
* Search for the issue to see if it is already solved, or if there is an open issue for it already. If there is an open issue already, you can comment on it if you have additional information that could be useful to us.
|
|
||||||
* For technical problems, discussion, questions and feature suggestions use the forum (https://forum.strawberrymusicplayer.org/) instead. The forum is better suited for discussion.
|
|
||||||
* We do not take feature requests from users on GitHub. Any issues related to feature requests will be closed. This does not necessarily mean that we won't add new features, but we don't have time to take feature requests or answer questions about new features from users. It is still possible to suggest or discuss new features on the forum (https://forum.strawberrymusicplayer.org/).
|
|
||||||
* We do not maintain the Flatpak package. Do not report issues related to Flatpak unless the issue can be reproduced with a native package, use Flatpak support instead https://flatpak.org/about/
|
|
||||||
|
|
||||||
### :moneybag: Sponsoring
|
This fork’s source (the code you are building here):
|
||||||
|
|
||||||
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.
|
- Fork: `https://gitea.dryark.com/dryark/strawberry`
|
||||||
There are currently 4 options for sponsoring:
|
|
||||||
|
|
||||||
1. [Patreon](https://www.patreon.com/jonaskvinge)
|
This fork is intended for people who want to:
|
||||||
2. [GitHub](https://github.com/sponsors/jonaski)
|
|
||||||
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
|
||||||
4. [PayPal](https://paypal.me/jonaskvinge)
|
|
||||||
|
|
||||||
Funding developers is a way to contribute to open source projects you appreciate, it helps developers get the resources they need, and recognize contributors working behind the scenes to make open source better for everyone.
|
- **build from source on macOS** without guesswork
|
||||||
|
- **produce signed + notarized binaries** themselves (and optionally distribute them)
|
||||||
|
|
||||||
### :heavy_check_mark: Features
|
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.
|
||||||
|
|
||||||
* Play and organize music
|
## Quick start (macOS)
|
||||||
* Supports WAV, FLAC, WavPack, Ogg FLAC, Ogg Vorbis, Ogg Opus, Ogg Speex, MPC, TrueAudio, AIFF, MP4, MP3, ASF and Monkey's Audio.
|
|
||||||
* Audio CD playback
|
|
||||||
* Native desktop notifications
|
|
||||||
* Playlist management
|
|
||||||
* Smart and dynamic playlists
|
|
||||||
* Advanced audio output and device configuration for bit-perfect playback on Linux
|
|
||||||
* In-player song loudness analysis and song playback loudness normalization, as per EBU R 128
|
|
||||||
* Edit tags on audio files
|
|
||||||
* Fetch tags from MusicBrainz
|
|
||||||
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
|
|
||||||
* Song lyrics from [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/)
|
|
||||||
* Support for multiple backends
|
|
||||||
* Audio analyzer
|
|
||||||
* Audio equalizer
|
|
||||||
* Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
|
||||||
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
|
||||||
* Streaming from Subsonic compatible servers
|
|
||||||
* Unofficial Tidal, Spotify and Qobuz integration
|
|
||||||
* Discord rich presence
|
|
||||||
|
|
||||||
|
Install Homebrew dependencies:
|
||||||
|
|
||||||
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
|
```bash
|
||||||
|
./build_tools/macos/install_brew_deps.sh
|
||||||
|
```
|
||||||
|
|
||||||
**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.**
|
Build:
|
||||||
|
|
||||||
### :heavy_exclamation_mark: Requirements
|
```bash
|
||||||
|
./build_tools/macos/build_app.sh --release --clean
|
||||||
|
open ./cmake-build-macos-release/strawberry.app
|
||||||
|
```
|
||||||
|
|
||||||
To build Strawberry from source you need the following installed on your system with the additional development packages/headers:
|
Build + deploy + sign + notarize (+ DMG):
|
||||||
|
|
||||||
* [CMake 3.13 or higher](https://cmake.org/)
|
```bash
|
||||||
* C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/) or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
|
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy --dmg \
|
||||||
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
--identity "Developer ID Application: Your Name (TEAMID)" \
|
||||||
* [Boost](https://www.boost.org/)
|
--notary-profile "<profile-name>"
|
||||||
* [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:
|
## Features
|
||||||
|
|
||||||
* Song fingerprinting and MusicBrainz tagging: [Chromaprint](https://acoustid.org/chromaprint)
|
- Play and organize your music collection
|
||||||
* Moodbar: [fftw3](http://www.fftw.org/)
|
- Supports formats: WAV, FLAC, WavPack, Ogg Vorbis, Opus, MPC, TrueAudio, AIFF, MP4, MP3, ASF, and Monkey’s Audio
|
||||||
* PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/?)
|
- Audio CD playback
|
||||||
* Audio CD: [libcdio](https://www.gnu.org/software/libcdio/)
|
- Bit-perfect playback on Linux
|
||||||
* MTP devices: [libmtp](http://libmtp.sourceforge.net/)
|
- Native desktop notifications
|
||||||
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
|
- Advanced playlist management
|
||||||
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
|
- Smart and dynamic playlists
|
||||||
* Discord rich presence [RapidJSON](https://rapidjson.org/)
|
- 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
|
||||||
|
|
||||||
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.
|
---
|
||||||
|
|
||||||
### :wrench: Build from source
|
:white_check_mark: Tested on **Linux**, **OpenBSD**, **FreeBSD**, **macOS**, and **Windows**.
|
||||||
|
|
||||||
### Get the code:
|
## :gear: Requirements
|
||||||
|
|
||||||
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
|
To build Strawberry from source, you’ll need:
|
||||||
|
|
||||||
### Build and install:
|
**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)
|
||||||
|
|
||||||
|
**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 (this fork):**
|
||||||
|
|
||||||
|
git clone --recursive https://gitea.dryark.com/dryark/strawberry
|
||||||
|
|
||||||
|
**Build and install:**
|
||||||
|
|
||||||
cd strawberry
|
cd strawberry
|
||||||
cmake -S . -B build
|
cmake -S . -B build
|
||||||
cmake --build build --parallel $(nproc)
|
cmake --build build --parallel $(nproc)
|
||||||
sudo cmake --install build
|
sudo cmake --install build
|
||||||
|
|
||||||
To build on Windows with Visual Studio 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
|
For building on Windows with Visual Studio 2022, see: :point_right: https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||||
|
|
||||||
### :penguin: Packaging status
|
---
|
||||||
|
|
||||||
|
## :package: Packaging status
|
||||||
|
|
||||||
[](https://repology.org/metapackage/strawberry/versions)
|
[](https://repology.org/metapackage/strawberry/versions)
|
||||||
|
|||||||
124
build_tools/README.md
Normal file
124
build_tools/README.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# 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>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## macOS Mac App Store (MAS) build + signed PKG
|
||||||
|
|
||||||
|
This repo includes `build_tools/macos/build_mas_pkg.sh` to automate:
|
||||||
|
|
||||||
|
- build (MAS mode) → deploy (bundle deps) → embed provisioning profile → codesign → `productbuild` a signed `.pkg`
|
||||||
|
|
||||||
|
### Requirements (Apple Developer)
|
||||||
|
|
||||||
|
- An App Store Connect app record with bundle id **`com.dryark.strawberry`** (or your own).
|
||||||
|
- A **Mac App Store provisioning profile** for that App ID.
|
||||||
|
- Signing identities installed in your Keychain:
|
||||||
|
- **Apple Distribution** (for the `.app`)
|
||||||
|
- **3rd Party Mac Developer Installer** (for the `.pkg`)
|
||||||
|
|
||||||
|
Tip: list what you have installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
security find-identity -p codesigning -v
|
||||||
|
security find-identity -p basic -v
|
||||||
|
ls -la "$HOME/Library/MobileDevice/Provisioning Profiles" | head -n 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual setup guide (certificates, Keychain Access, profiles)
|
||||||
|
|
||||||
|
See: `build_tools/macos/README_MAS.md`
|
||||||
|
|
||||||
|
### Build the signed upload PKG
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
||||||
|
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
||||||
|
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
||||||
|
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
- `cmake-build-macos-release-mas/strawberry.app`
|
||||||
|
- `cmake-build-macos-release-mas/strawberry-mas.pkg`
|
||||||
|
|
||||||
|
### Upload + submit for review
|
||||||
|
|
||||||
|
- Upload the `.pkg` using Apple’s **Transporter** app (App Store Connect), or with `iTMSTransporter`.
|
||||||
|
- In App Store Connect, wait for processing, select the build, then **Submit for Review**.
|
||||||
421
build_tools/macos/README_MAS.md
Normal file
421
build_tools/macos/README_MAS.md
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
# Mac App Store (MAS) submission guide (manual steps)
|
||||||
|
|
||||||
|
This repo supports a **Mac App Store build mode** (`BUILD_FOR_MAC_APP_STORE=ON`) and includes scripts to build a signed upload `.pkg`.
|
||||||
|
|
||||||
|
If you’re blocked because `security find-identity` only shows **Developer ID** and not **Apple Distribution / Installer**, follow the steps below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Keychain Access (macOS “hidden” Utilities)
|
||||||
|
|
||||||
|
Any of these work:
|
||||||
|
|
||||||
|
- **Spotlight**: press `⌘ + Space` → type **Keychain Access** → Enter
|
||||||
|
- **Finder**: Applications → Utilities → **Keychain Access**
|
||||||
|
- **Terminal**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open -a "Keychain Access"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The core issue: certificate exists but is not a usable identity
|
||||||
|
|
||||||
|
If you see certificates like:
|
||||||
|
|
||||||
|
- `Apple Distribution: ...`
|
||||||
|
- `3rd Party Mac Developer Installer: ...`
|
||||||
|
|
||||||
|
but `security find-identity` does **not** list them, then the certificate is present but **the private key is missing** (or not paired / in the wrong keychain).
|
||||||
|
|
||||||
|
You can confirm with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_tools/macos/check_signing_identities.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 — Create the private keys on this Mac (CSR)
|
||||||
|
|
||||||
|
1. Open **Keychain Access**
|
||||||
|
2. Menu: **Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority…**
|
||||||
|
3. Fill:
|
||||||
|
- **User Email Address**: your Apple ID email
|
||||||
|
- **Common Name**: e.g. `Dry Ark LLC` (any label is fine)
|
||||||
|
- **CA Email Address**: leave blank
|
||||||
|
- Select: **Saved to disk**
|
||||||
|
4. Save the CSR (`.certSigningRequest`) somewhere safe
|
||||||
|
|
||||||
|
This CSR step is what creates the **private key** locally in your login keychain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — Create + download the certificates (Apple Developer portal)
|
||||||
|
|
||||||
|
In Apple Developer → **Certificates, Identifiers & Profiles** → **Certificates** → **+**:
|
||||||
|
|
||||||
|
- Create **Apple Distribution** (use the CSR you just made)
|
||||||
|
- Create **Mac Installer Distribution** (or “3rd Party Mac Developer Installer”, wording varies) (use a CSR)
|
||||||
|
|
||||||
|
Download the resulting `.cer` files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Install certificates into your login keychain
|
||||||
|
|
||||||
|
Double-click each downloaded `.cer` to install it.
|
||||||
|
|
||||||
|
Then in **Keychain Access → login → My Certificates**:
|
||||||
|
|
||||||
|
- Find **Apple Distribution: ...** and **expand it**
|
||||||
|
- You must see a **private key** under it.
|
||||||
|
- Find **... Installer ...** and expand it
|
||||||
|
- You must see a **private key** under it.
|
||||||
|
|
||||||
|
If there’s no private key under the certificate, it will not be usable for signing on this Mac.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — Verify identities from the CLI
|
||||||
|
|
||||||
|
### Common failure: errSecInternalComponent / chain-to-root warnings
|
||||||
|
|
||||||
|
If you see errors like:
|
||||||
|
|
||||||
|
- `Warning: unable to build chain to self-signed root for signer "Apple Distribution: ..."`
|
||||||
|
- `errSecInternalComponent`
|
||||||
|
|
||||||
|
This is almost always a **keychain search list / trust chain** issue.
|
||||||
|
|
||||||
|
#### Important: do NOT “Always Trust” your Apple Distribution / Installer certs
|
||||||
|
|
||||||
|
Setting your leaf signing certificates (e.g. **Apple Distribution** / **3rd Party Mac Developer Installer**) to **Always Trust** can make things worse by overriding the normal trust chain and causing codesign to fail chain building.
|
||||||
|
|
||||||
|
If you changed trust settings:
|
||||||
|
|
||||||
|
- In **Keychain Access → login → My Certificates**
|
||||||
|
- open the cert → **Trust**
|
||||||
|
- set **“When using this certificate” = “Use System Defaults”**
|
||||||
|
|
||||||
|
Fix (safe, common): ensure the System keychains are included in the user search list:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
security list-keychains -d user
|
||||||
|
security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db" "/Library/Keychains/System.keychain" "/System/Library/Keychains/SystemRootCertificates.keychain"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then re-run the build/sign script.
|
||||||
|
|
||||||
|
#### Install the correct Apple intermediate certificates (WWDR)
|
||||||
|
|
||||||
|
If the System keychains are already in the search list and you still get chain errors, you’re likely missing an Apple intermediate (commonly **WWDR**).
|
||||||
|
|
||||||
|
Download the current Apple WWDR intermediate certificate(s) from Apple’s official Certificate Authority page:
|
||||||
|
|
||||||
|
- `https://www.apple.com/certificateauthority/`
|
||||||
|
|
||||||
|
Then import into the **System** keychain (recommended):
|
||||||
|
|
||||||
|
- Keychain Access → **System** keychain → File → **Import Items…** → select the downloaded `.cer`
|
||||||
|
|
||||||
|
Or via CLI (requires admin):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo security add-certificates -k /Library/Keychains/System.keychain "/path/to/WWDR.cer"
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify it’s visible:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
security find-certificate -a -c "Apple Worldwide Developer Relations" /Library/Keychains/System.keychain | head -n 10
|
||||||
|
```
|
||||||
|
|
||||||
|
If needed, you can also verify the chain for your distribution cert:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
security verify-cert -c "Apple Distribution: Dry Ark LLC (7628766FL2)" 2>&1 | head -n 80
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
security find-identity -p codesigning -v
|
||||||
|
security find-identity -p basic -v
|
||||||
|
./build_tools/macos/check_signing_identities.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- `Apple Distribution: ...` shows up under **codesigning**
|
||||||
|
- `... Installer ...` shows up as an **installer identity** (used to sign upload `.pkg`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 — Create + install the provisioning profile (Mac App Store)
|
||||||
|
|
||||||
|
In Apple Developer → **Profiles** → **+**:
|
||||||
|
|
||||||
|
- Platform: **macOS**
|
||||||
|
- Type: **Mac App Store**
|
||||||
|
- App ID: `com.dryark.strawberry` (or your own bundle id)
|
||||||
|
- Select the **Apple Distribution** certificate
|
||||||
|
- Generate + Download
|
||||||
|
|
||||||
|
### Where the `.provisionprofile` ends up (newer Xcode/macOS)
|
||||||
|
|
||||||
|
Recent Xcode versions store “downloaded manual profiles” under:
|
||||||
|
|
||||||
|
- `~/Library/Developer/Xcode/UserData/Provisioning Profiles/`
|
||||||
|
|
||||||
|
Older tooling sometimes used:
|
||||||
|
|
||||||
|
- `~/Library/MobileDevice/Provisioning Profiles/`
|
||||||
|
|
||||||
|
This repo’s MAS build script does **not** require the profile to be in a specific folder — you can pass the path directly.
|
||||||
|
|
||||||
|
To locate and pick the right profile, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id com.dryark.strawberry
|
||||||
|
```
|
||||||
|
|
||||||
|
### (Optional) Copy to the legacy folder
|
||||||
|
|
||||||
|
If some other tools expect the legacy folder, you can copy it there:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||||
|
cp -f "/path/to/profile.provisionprofile" "$HOME/Library/MobileDevice/Provisioning Profiles/"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6 — Build the signed upload package (.pkg)
|
||||||
|
|
||||||
|
This repo provides:
|
||||||
|
|
||||||
|
- `build_tools/macos/build_mas_pkg.sh` (build → deploy → embed profile → sign → productbuild)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
||||||
|
--codesign-identity "Apple Distribution: Dry Ark LLC (7628766FL2)" \
|
||||||
|
--installer-identity "3rd Party Mac Developer Installer: Dry Ark LLC (7628766FL2)" \
|
||||||
|
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
|
||||||
|
- `cmake-build-macos-release-mas/strawberry.app`
|
||||||
|
- `cmake-build-macos-release-mas/strawberry-mas.pkg`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture note — arm64 vs universal (arm64+x86_64)
|
||||||
|
|
||||||
|
For Mac App Store uploads, your `.pkg` can contain either:
|
||||||
|
|
||||||
|
- **arm64-only** app (Apple Silicon only), or
|
||||||
|
- **universal** app (arm64 + x86_64), or
|
||||||
|
- **x86_64-only** app (runs on Apple Silicon under Rosetta 2, but native Intel only otherwise)
|
||||||
|
|
||||||
|
Apple does **not** require universal binaries for review. **arm64-only is allowed**, but:
|
||||||
|
|
||||||
|
- Intel Macs **cannot** run an arm64-only app.
|
||||||
|
- If you ship arm64-only, App Store Connect will effectively make the app available only to Apple Silicon Macs (and your listing will reflect that).
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
|
||||||
|
- If you want the broadest compatibility, aim for a **universal build**.
|
||||||
|
- If you’re okay supporting only Apple Silicon Macs, arm64-only is the simplest path.
|
||||||
|
|
||||||
|
### Can I upload two different `.pkg`s (one arm64, one x86_64)?
|
||||||
|
|
||||||
|
Not in the way you want.
|
||||||
|
|
||||||
|
- In App Store Connect you can upload multiple builds over time, but for any given version/submission you ultimately pick **one build** to submit.
|
||||||
|
- Apple will not “merge” two separate uploads (arm64-only + x86_64-only) into one app for customers.
|
||||||
|
|
||||||
|
If you want both Apple Silicon and Intel supported **natively**, you need to produce a **single universal** app bundle and package that into **one** `.pkg`.
|
||||||
|
|
||||||
|
If you don’t want to deal with universal yet, your practical choices are:
|
||||||
|
|
||||||
|
- **arm64-only**: Apple Silicon only.
|
||||||
|
- **x86_64-only**: runs on Intel natively, and on Apple Silicon under **Rosetta 2** (slower, but widely compatible).
|
||||||
|
|
||||||
|
### Practical reality for this repo
|
||||||
|
|
||||||
|
This project depends on large native dependency stacks (Qt, GStreamer, plugins). If you build those via Homebrew, you typically end up with **single-architecture** libraries (arm64 under `/opt/homebrew`, x86_64 under `/usr/local`).
|
||||||
|
|
||||||
|
A true universal app requires **all bundled native code** (your executable *and* all `.dylib`/plugins/frameworks you ship) to be universal as well.
|
||||||
|
|
||||||
|
If you decide you want universal:
|
||||||
|
|
||||||
|
- You’ll need a universal build of **Qt** and **GStreamer** (and all bundled plugins), or
|
||||||
|
- Build arm64 and x86_64 bundles separately and combine *matching* binaries where possible (advanced; easy to break signing / plugin loading).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting — `productbuild` fails with CSSM `-60008` (authorization)
|
||||||
|
|
||||||
|
If you see something like:
|
||||||
|
|
||||||
|
- `SignData failed ... CSSM Exception: -60008 Unable to obtain authorization for this operation`
|
||||||
|
|
||||||
|
That means the **Installer** certificate is present, but macOS is not allowing `productbuild` to use the **private key** without additional authorization.
|
||||||
|
|
||||||
|
### Fix option A (recommended): set key partition list (CLI)
|
||||||
|
|
||||||
|
This is the standard “allow Apple tools to sign without GUI prompts” fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
security unlock-keychain "$HOME/Library/Keychains/login.keychain-db"
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "<login-keychain-password>" "$HOME/Library/Keychains/login.keychain-db"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: if your password contains characters like `!` or `$` and you paste it into a command in `zsh`,
|
||||||
|
the shell can modify it (history/variable expansion) and `security ... -k` may claim it’s “incorrect”.
|
||||||
|
Use **single quotes** (or the env var path shown below) to avoid this, e.g.:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k 'p@ssw0rd!$' "$HOME/Library/Keychains/login.keychain-db"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then rerun:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_tools/macos/build_mas_pkg.sh --run ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This repo’s script also supports:
|
||||||
|
|
||||||
|
- `--keychain-password <pw>` (or env var `STRAWBERRY_KEYCHAIN_PASSWORD`)
|
||||||
|
|
||||||
|
### Fix option B: Keychain Access UI (one-time)
|
||||||
|
|
||||||
|
1. Open **Keychain Access**
|
||||||
|
2. Select **login** keychain → **My Certificates**
|
||||||
|
3. Find your installer cert (e.g. `3rd Party Mac Developer Installer: ...`) and **expand it**
|
||||||
|
4. Select the **private key** under it
|
||||||
|
5. **Get Info → Access Control**
|
||||||
|
- Add `/usr/bin/productbuild` (and optionally `/usr/bin/pkgbuild`) to the allowed apps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7 — Upload + submit for review
|
||||||
|
|
||||||
|
### 7.1 Install Apple “Transporter” (the upload tool)
|
||||||
|
|
||||||
|
Apple requires Mac App Store submissions to be uploaded using **Transporter** (a macOS app published by Apple).
|
||||||
|
|
||||||
|
Where to get it:
|
||||||
|
|
||||||
|
- Install **Transporter** from the **Mac App Store** (search for “Transporter”).
|
||||||
|
- App Store listing name is typically **“Transporter”** by Apple.
|
||||||
|
|
||||||
|
### 7.2 Upload the `.pkg` with Transporter
|
||||||
|
|
||||||
|
1. Open **Transporter**
|
||||||
|
2. Sign in with the Apple ID that has access to **App Store Connect**
|
||||||
|
3. Click **Add App** (or **+**) and choose your signed upload package:
|
||||||
|
- `cmake-build-macos-release-mas/strawberry-mas.pkg` (or your custom `--pkg-out` path)
|
||||||
|
4. Click **Deliver**
|
||||||
|
5. Wait for upload + server-side validation to complete
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Uploading can take a while depending on your connection.
|
||||||
|
- If Transporter reports an error, the message usually includes the exact App Store Connect requirement you violated (bundle id mismatch, missing entitlements, invalid signature, etc.).
|
||||||
|
|
||||||
|
### 7.3 Submit the build in App Store Connect
|
||||||
|
|
||||||
|
1. Open **App Store Connect** in your browser and go to **My Apps**
|
||||||
|
2. Select your app, then go to the **macOS App** platform section
|
||||||
|
3. Find your uploaded build under **TestFlight** or **Prepare for Submission** (Apple’s UI wording changes over time)
|
||||||
|
4. Wait for Apple to finish “Processing” the build
|
||||||
|
5. Select the build for your version, complete required metadata, then click **Submit for Review**
|
||||||
|
|
||||||
|
### (Optional) CLI upload (advanced): `iTMSTransporter`
|
||||||
|
|
||||||
|
If you prefer uploading from the command line, Apple’s underlying uploader is **iTMSTransporter**.
|
||||||
|
On most systems it’s available via Xcode command line tools as:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun iTMSTransporter -help
|
||||||
|
```
|
||||||
|
|
||||||
|
CLI upload requires additional credentials (App Store Connect API key or Apple ID auth) and is easier to get wrong than the Transporter GUI.
|
||||||
|
For most folks, **Transporter.app is the recommended path**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating a universal Mac App Store upload using two Macs (arm64 + x86_64)
|
||||||
|
|
||||||
|
If you have both an Apple Silicon Mac and an Intel Mac, the most reliable way to ship a universal app for this repo is:
|
||||||
|
|
||||||
|
1. Build + deploy the **unsigned** MAS app bundle on each machine (arm64 and x86_64).
|
||||||
|
2. Copy both `.app` bundles to the machine that has your signing keys.
|
||||||
|
3. Merge them with `lipo` and then **sign + package** once, producing a single universal `.pkg`.
|
||||||
|
|
||||||
|
### Step A — Build + deploy (arm64 machine)
|
||||||
|
|
||||||
|
On your Apple Silicon Mac:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_tools/macos/build_app.sh --release --clean --mas --deploy --build-dir ./cmake-build-macos-release-mas-arm64
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces (unsigned):
|
||||||
|
|
||||||
|
- `cmake-build-macos-release-mas-arm64/strawberry.app`
|
||||||
|
|
||||||
|
### Step B — Build + deploy (x86_64 machine)
|
||||||
|
|
||||||
|
On your Intel Mac:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_tools/macos/build_app.sh --release --clean --mas --deploy --build-dir ./cmake-build-macos-release-mas-x86_64
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces (unsigned):
|
||||||
|
|
||||||
|
- `cmake-build-macos-release-mas-x86_64/strawberry.app`
|
||||||
|
|
||||||
|
### Step C — Copy both app bundles to one “packaging” machine
|
||||||
|
|
||||||
|
Pick the Mac that has your **Apple Distribution** and **Installer** identities (private keys) installed.
|
||||||
|
Copy both `.app` bundles onto that Mac, for example:
|
||||||
|
|
||||||
|
- `/path/to/inputs/strawberry-arm64.app`
|
||||||
|
- `/path/to/inputs/strawberry-x86_64.app`
|
||||||
|
|
||||||
|
Tip: `rsync` works well for app bundles:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsync -a "/path/to/arm64/strawberry.app" "/path/to/inputs/strawberry-arm64.app"
|
||||||
|
rsync -a "/path/to/x86_64/strawberry.app" "/path/to/inputs/strawberry-x86_64.app"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step D — Merge + sign + build the universal `.pkg`
|
||||||
|
|
||||||
|
On the packaging machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_tools/macos/build_mas_universal_pkg.sh --run \
|
||||||
|
--arm-app "/path/to/inputs/strawberry-arm64.app" \
|
||||||
|
--x86-app "/path/to/inputs/strawberry-x86_64.app" \
|
||||||
|
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
||||||
|
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
||||||
|
--provisionprofile "/path/to/profile.provisionprofile"
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
|
||||||
|
- `cmake-build-macos-release-mas-universal/strawberry.app` (universal)
|
||||||
|
- `cmake-build-macos-release-mas-universal/strawberry-mas-universal.pkg`
|
||||||
|
|
||||||
|
### Important constraints (don’t skip)
|
||||||
|
|
||||||
|
- The two input apps must be built from the **same commit** with the **same enabled features** so the app bundle layouts match.
|
||||||
|
- Do **not** sign the per-arch apps first; `lipo` invalidates signatures. Sign **only after** merging.
|
||||||
277
build_tools/macos/build_app.sh
Executable file
277
build_tools/macos/build_app.sh
Executable file
@@ -0,0 +1,277 @@
|
|||||||
|
#!/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] [--mas] [--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
|
||||||
|
--mas Build for Mac App Store (BUILD_FOR_MAC_APP_STORE=ON). Disables Sparkle/QtSparkle and any localhost OAuth redirect listener.
|
||||||
|
--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_mas=0
|
||||||
|
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 ;;
|
||||||
|
--mas) do_mas=1; 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_mas" -eq 1 ]]; then
|
||||||
|
echo "==> [$(ts)] MAS: enabled (BUILD_FOR_MAC_APP_STORE=ON)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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=()
|
||||||
|
|
||||||
|
# Mac App Store build mode
|
||||||
|
if [[ "$do_mas" -eq 1 ]]; then
|
||||||
|
cmake_extra_args+=("-DBUILD_FOR_MAC_APP_STORE=ON")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
324
build_tools/macos/build_mas_pkg.sh
Executable file
324
build_tools/macos/build_mas_pkg.sh
Executable file
@@ -0,0 +1,324 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Guard: this script must be executed with bash (not sourced into zsh, not run via sh).
|
||||||
|
if [[ -z "${BASH_VERSION:-}" ]]; then
|
||||||
|
echo "Error: this script must be run with bash (it uses bash arrays)." >&2
|
||||||
|
echo "Run:" >&2
|
||||||
|
echo " bash ./build_tools/macos/build_mas_pkg.sh --run ..." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
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)"
|
||||||
|
|
||||||
|
prepare_login_keychain_for_signing() {
|
||||||
|
# Some setups require explicitly granting Apple tooling access to the private key(s)
|
||||||
|
# (otherwise productbuild/codesign can fail with authorization errors like:
|
||||||
|
# CSSM Exception: -60008 Unable to obtain authorization for this operation
|
||||||
|
# or "User interaction is not allowed").
|
||||||
|
#
|
||||||
|
# This function is optional and only runs if a keychain password is provided.
|
||||||
|
local keychain_path="$HOME/Library/Keychains/login.keychain-db"
|
||||||
|
local pw="${1:-}"
|
||||||
|
|
||||||
|
if [[ -z "$pw" ]]; then
|
||||||
|
echo "==> [$(ts)] Note: no keychain password provided; skipping keychain access-control preparation."
|
||||||
|
echo " If productbuild later fails with -60008 authorization errors, fix it with either:"
|
||||||
|
echo " - Keychain Access → login → My Certificates → select the *private key* under the Installer cert → Get Info → Access Control → allow productbuild"
|
||||||
|
echo " - OR (CLI): security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k <password> \"$keychain_path\""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Preparing login keychain for signing (unlock + key partition list)"
|
||||||
|
# Unlock so Security/Authorization can use keys without prompting.
|
||||||
|
security unlock-keychain -p "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
||||||
|
# Allow Apple tools (codesign/productbuild) to access the private key without GUI prompts.
|
||||||
|
# This is the standard fix used for non-interactive signing.
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_keychain_search_list() {
|
||||||
|
# codesign builds the cert chain using the user keychain search list.
|
||||||
|
# If the list is missing the System keychain, you can get:
|
||||||
|
# "unable to build chain to self-signed root" + errSecInternalComponent
|
||||||
|
local login_kc="$HOME/Library/Keychains/login.keychain-db"
|
||||||
|
local system_kc="/Library/Keychains/System.keychain"
|
||||||
|
local roots_kc="/System/Library/Keychains/SystemRootCertificates.keychain"
|
||||||
|
|
||||||
|
local current
|
||||||
|
current="$(security list-keychains -d user 2>/dev/null | tr -d '"' | tr -d ' ' || true)"
|
||||||
|
|
||||||
|
if echo "$current" | grep -Fq "$system_kc"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Note: adding System keychains to the user keychain search list (fixes common codesign chain errors)"
|
||||||
|
echo " (This changes the user keychain search list; run 'security list-keychains -d user' to view.)"
|
||||||
|
security list-keychains -d user -s "$login_kc" "$system_kc" "$roots_kc" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnose_chain_failure() {
|
||||||
|
echo "==> [$(ts)] Codesign failed. Common causes on macOS:"
|
||||||
|
echo " - System keychains not in the user keychain search list"
|
||||||
|
echo " - Missing/invalid WWDR intermediate certificate"
|
||||||
|
echo " - Keychain/key access issues"
|
||||||
|
echo
|
||||||
|
echo "==> [$(ts)] Keychain search list:"
|
||||||
|
security list-keychains -d user || true
|
||||||
|
echo
|
||||||
|
echo "==> [$(ts)] Checking for Apple WWDR intermediate in System keychain:"
|
||||||
|
security find-certificate -a -c "Apple Worldwide Developer Relations" /Library/Keychains/System.keychain 2>/dev/null | head -n 5 || true
|
||||||
|
echo
|
||||||
|
echo "==> [$(ts)] If WWDR is missing, install the current Apple WWDR intermediate certificate (via Xcode or Apple Developer portal)."
|
||||||
|
echo "==> [$(ts)] Then re-run this script."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
preflight_identity() {
|
||||||
|
local what="$1"
|
||||||
|
local policy="$2"
|
||||||
|
local identity="$3"
|
||||||
|
|
||||||
|
# NOTE: security expects "-p <policy>" as *two* args; do not pass "-p codesigning" as one string.
|
||||||
|
if ! security find-identity -p "$policy" -v 2>/dev/null | grep -Fq "$identity"; then
|
||||||
|
echo "Error: ${what} identity not found/usable in Keychain: $identity" >&2
|
||||||
|
echo "Run: ./build_tools/macos/check_signing_identities.sh" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./build_tools/macos/build_mas_pkg.sh --run [options]
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
- Builds Strawberry in Mac App Store mode (BUILD_FOR_MAC_APP_STORE=ON)
|
||||||
|
- Runs deploy (macdeployqt + bundling) so the app bundle is self-contained
|
||||||
|
- Embeds a Mac App Store provisioning profile into the app bundle
|
||||||
|
- Codesigns the app with an Apple Distribution identity + entitlements
|
||||||
|
- Builds a signed .pkg suitable for uploading to App Store Connect
|
||||||
|
|
||||||
|
Required options:
|
||||||
|
--run
|
||||||
|
--codesign-identity "<name>" (e.g. "Apple Distribution: Dry Ark LLC (TEAMID)")
|
||||||
|
--installer-identity "<name>" (e.g. "3rd Party Mac Developer Installer: Dry Ark LLC (TEAMID)")
|
||||||
|
--provisionprofile <path> Path to a *Mac App Store* provisioning profile (*.provisionprofile)
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--release | --debug Build config (default: Release)
|
||||||
|
--clean Clean build dir before build
|
||||||
|
--build-dir <path> Override build directory
|
||||||
|
--entitlements <plist> Codesign entitlements (default: dist/macos/entitlements.mas.plist)
|
||||||
|
--bundle-id <id> Override CFBundleIdentifier (default: com.dryark.strawberry)
|
||||||
|
--pkg-out <path> Output .pkg path (default: <build-dir>/strawberry-mas.pkg)
|
||||||
|
--keychain-password <pw> OPTIONAL: unlock/login keychain + set key partition list for Apple tools
|
||||||
|
(alternative: set env var STRAWBERRY_KEYCHAIN_PASSWORD)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Tip: if your keychain password contains characters like ! or $, prefer the env var or single quotes.
|
||||||
|
STRAWBERRY_KEYCHAIN_PASSWORD='your-login-keychain-password' \
|
||||||
|
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
||||||
|
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
||||||
|
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
||||||
|
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
||||||
|
|
||||||
|
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
||||||
|
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
||||||
|
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
||||||
|
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Mac App Store submissions do NOT use Developer ID notarization.
|
||||||
|
- You must create a Mac App Store provisioning profile for your App ID in Apple Developer.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||||
|
echo "Error: This script is for macOS only." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
do_run=0
|
||||||
|
config="Release"
|
||||||
|
do_clean=0
|
||||||
|
build_dir=""
|
||||||
|
codesign_identity=""
|
||||||
|
installer_identity=""
|
||||||
|
provisionprofile=""
|
||||||
|
entitlements=""
|
||||||
|
bundle_id="com.dryark.strawberry"
|
||||||
|
pkg_out=""
|
||||||
|
keychain_password="${STRAWBERRY_KEYCHAIN_PASSWORD:-}"
|
||||||
|
|
||||||
|
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 ;;
|
||||||
|
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
||||||
|
--codesign-identity) codesign_identity="${2:-}"; shift 2 ;;
|
||||||
|
--installer-identity) installer_identity="${2:-}"; shift 2 ;;
|
||||||
|
--provisionprofile) provisionprofile="${2:-}"; shift 2 ;;
|
||||||
|
--entitlements) entitlements="${2:-}"; shift 2 ;;
|
||||||
|
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
||||||
|
--pkg-out) pkg_out="${2:-}"; shift 2 ;;
|
||||||
|
--keychain-password) keychain_password="${2:-}"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$do_run" -eq 0 ]]; then
|
||||||
|
usage
|
||||||
|
echo
|
||||||
|
echo "==> [$(ts)] Tip: list available signing identities:"
|
||||||
|
echo " security find-identity -p codesigning -v"
|
||||||
|
echo " security find-identity -p basic -v"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$codesign_identity" ]]; then
|
||||||
|
echo "Error: missing --codesign-identity" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ -z "$installer_identity" ]]; then
|
||||||
|
echo "Error: missing --installer-identity" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ -z "$provisionprofile" || ! -f "$provisionprofile" ]]; then
|
||||||
|
echo "Error: missing/invalid --provisionprofile: $provisionprofile" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$entitlements" ]]; then
|
||||||
|
entitlements="${repo_root}/dist/macos/entitlements.mas.plist"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$entitlements" ]]; then
|
||||||
|
echo "Error: entitlements file not found: $entitlements" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$build_dir" ]]; then
|
||||||
|
build_dir="${repo_root}/cmake-build-macos-$(lower "$config")-mas"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$pkg_out" ]]; then
|
||||||
|
pkg_out="${build_dir}/strawberry-mas.pkg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Repo: ${repo_root}"
|
||||||
|
echo "==> [$(ts)] Build dir: ${build_dir}"
|
||||||
|
echo "==> [$(ts)] Config: ${config}"
|
||||||
|
echo "==> [$(ts)] Bundle ID: ${bundle_id}"
|
||||||
|
echo "==> [$(ts)] Entitlements: ${entitlements}"
|
||||||
|
echo "==> [$(ts)] Provisioning profile: ${provisionprofile}"
|
||||||
|
echo "==> [$(ts)] Output pkg: ${pkg_out}"
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Building (Mac App Store mode)"
|
||||||
|
build_args=( "--release" )
|
||||||
|
if [[ "$config" == "Debug" ]]; then build_args=( "--debug" ); fi
|
||||||
|
if [[ "$do_clean" -eq 1 ]]; then build_args+=( "--clean" ); fi
|
||||||
|
build_args+=( "--build-dir" "$build_dir" "--mas" "--deploy" )
|
||||||
|
|
||||||
|
# Provide bundle id via CMake cache variable.
|
||||||
|
export MACOS_BUNDLE_ID="$bundle_id"
|
||||||
|
|
||||||
|
"${repo_root}/build_tools/macos/build_app.sh" "${build_args[@]}"
|
||||||
|
|
||||||
|
app_path="${build_dir}/strawberry.app"
|
||||||
|
bin_path="${app_path}/Contents/MacOS/strawberry"
|
||||||
|
if [[ ! -x "$bin_path" ]]; then
|
||||||
|
echo "Error: built app not found at: $app_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Embedding provisioning profile"
|
||||||
|
cp -f "$provisionprofile" "${app_path}/Contents/embedded.provisionprofile"
|
||||||
|
|
||||||
|
ensure_keychain_search_list
|
||||||
|
prepare_login_keychain_for_signing "$keychain_password"
|
||||||
|
preflight_identity "codesign" "codesigning" "$codesign_identity"
|
||||||
|
preflight_identity "installer" "basic" "$installer_identity"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Codesigning app (Mac App Store)"
|
||||||
|
codesign_args=( --force --timestamp --options runtime --sign "$codesign_identity" --entitlements "$entitlements" )
|
||||||
|
|
||||||
|
# Clean up any leftover codesign temp files from previous interrupted runs.
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
# Only sign Mach-O binaries.
|
||||||
|
if file -b "$f" | grep -q "Mach-O"; then
|
||||||
|
codesign "${codesign_args[@]}" "$f" >/dev/null
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
codesign "${codesign_args[@]}" "$app_path" >/dev/null
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Verifying codesign"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$app_path"
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Building signed .pkg for App Store upload"
|
||||||
|
rm -f "$pkg_out" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
if ! productbuild \
|
||||||
|
--component "$app_path" /Applications \
|
||||||
|
--sign "$installer_identity" \
|
||||||
|
"$pkg_out"; then
|
||||||
|
echo "Error: productbuild failed while signing the .pkg." >&2
|
||||||
|
echo "Common cause: keychain/private-key authorization (e.g. CSSM -60008)." >&2
|
||||||
|
echo >&2
|
||||||
|
echo "Fix options:" >&2
|
||||||
|
echo "1) Keychain Access UI:" >&2
|
||||||
|
echo " - Keychain Access → login → My Certificates" >&2
|
||||||
|
echo " - Find: $installer_identity" >&2
|
||||||
|
echo " - Expand it and select the *private key* under it" >&2
|
||||||
|
echo " - Get Info → Access Control → allow /usr/bin/productbuild (optionally also allow /usr/bin/pkgbuild)" >&2
|
||||||
|
echo "2) CLI (recommended for repeatable builds):" >&2
|
||||||
|
echo " security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k <password> \"$HOME/Library/Keychains/login.keychain-db\"" >&2
|
||||||
|
echo >&2
|
||||||
|
echo "Tip: you can also rerun this script with:" >&2
|
||||||
|
echo " --keychain-password <login-keychain-password>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Verifying pkg signature"
|
||||||
|
pkgutil --check-signature "$pkg_out" || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done."
|
||||||
|
echo "App: $app_path"
|
||||||
|
echo "PKG: $pkg_out"
|
||||||
|
|
||||||
241
build_tools/macos/build_mas_universal_pkg.sh
Normal file
241
build_tools/macos/build_mas_universal_pkg.sh
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Build a universal (arm64+x86_64) Mac App Store upload package by:
|
||||||
|
# - merging two already-deployed Strawberry.app bundles (arm64 + x86_64) using lipo
|
||||||
|
# - embedding a Mac App Store provisioning profile
|
||||||
|
# - codesigning with Apple Distribution (+ entitlements)
|
||||||
|
# - producing a signed .pkg with productbuild (Installer identity)
|
||||||
|
#
|
||||||
|
# Intended workflow with two Macs:
|
||||||
|
# 1) On Apple Silicon Mac: build+deploy MAS app bundle (unsigned) → copy strawberry.app somewhere
|
||||||
|
# 2) On Intel Mac: build+deploy MAS app bundle (unsigned) → copy strawberry.app somewhere
|
||||||
|
# 3) On the Mac that holds your signing keys (either one): run THIS script to merge+sign+package
|
||||||
|
|
||||||
|
ts() { date +"%H:%M:%S"; }
|
||||||
|
|
||||||
|
if [[ -z "${BASH_VERSION:-}" ]]; then
|
||||||
|
echo "Error: this script must be run with bash." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||||
|
echo "Error: This script is for macOS only." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
||||||
|
|
||||||
|
ensure_keychain_search_list() {
|
||||||
|
local login_kc="$HOME/Library/Keychains/login.keychain-db"
|
||||||
|
local system_kc="/Library/Keychains/System.keychain"
|
||||||
|
local roots_kc="/System/Library/Keychains/SystemRootCertificates.keychain"
|
||||||
|
|
||||||
|
local current
|
||||||
|
current="$(security list-keychains -d user 2>/dev/null | tr -d '"' | tr -d ' ' || true)"
|
||||||
|
if echo "$current" | grep -Fq "$system_kc"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "==> [$(ts)] Note: adding System keychains to the user keychain search list"
|
||||||
|
security list-keychains -d user -s "$login_kc" "$system_kc" "$roots_kc" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_login_keychain_for_signing() {
|
||||||
|
local keychain_path="$HOME/Library/Keychains/login.keychain-db"
|
||||||
|
local pw="${1:-}"
|
||||||
|
if [[ -z "$pw" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "==> [$(ts)] Preparing login keychain for signing (unlock + key partition list)"
|
||||||
|
security unlock-keychain -p "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
preflight_identity() {
|
||||||
|
local what="$1"
|
||||||
|
local policy="$2"
|
||||||
|
local identity="$3"
|
||||||
|
if ! security find-identity -p "$policy" -v 2>/dev/null | grep -Fq "$identity"; then
|
||||||
|
echo "Error: ${what} identity not found/usable in Keychain: $identity" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./build_tools/macos/build_mas_universal_pkg.sh --run [options]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--run
|
||||||
|
--arm-app <path> Path to arm64 Strawberry.app (already built+deployed, unsigned)
|
||||||
|
--x86-app <path> Path to x86_64 Strawberry.app (already built+deployed, unsigned)
|
||||||
|
--codesign-identity "<name>" Apple Distribution: ...
|
||||||
|
--installer-identity "<name>" 3rd Party Mac Developer Installer: ...
|
||||||
|
--provisionprofile <path> Mac App Store provisioning profile (*.provisionprofile)
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--out-dir <path> Output directory (default: cmake-build-macos-release-mas-universal)
|
||||||
|
--entitlements <plist> Codesign entitlements (default: dist/macos/entitlements.mas.plist)
|
||||||
|
--pkg-out <path> Output .pkg path (default: <out-dir>/strawberry-mas-universal.pkg)
|
||||||
|
--bundle-id <id> For display/logging only (does not rewrite Info.plist)
|
||||||
|
--keychain-password <pw> Or set env var STRAWBERRY_KEYCHAIN_PASSWORD (quote it!)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- This script does NOT build Strawberry. It merges two pre-built app bundles.
|
||||||
|
- After lipo-merging, the app must be re-signed (this script does that).
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
do_run=0
|
||||||
|
arm_app=""
|
||||||
|
x86_app=""
|
||||||
|
out_dir=""
|
||||||
|
codesign_identity=""
|
||||||
|
installer_identity=""
|
||||||
|
provisionprofile=""
|
||||||
|
entitlements=""
|
||||||
|
pkg_out=""
|
||||||
|
bundle_id=""
|
||||||
|
keychain_password="${STRAWBERRY_KEYCHAIN_PASSWORD:-}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--run) do_run=1; shift ;;
|
||||||
|
--arm-app) arm_app="${2:-}"; shift 2 ;;
|
||||||
|
--x86-app) x86_app="${2:-}"; shift 2 ;;
|
||||||
|
--out-dir) out_dir="${2:-}"; shift 2 ;;
|
||||||
|
--codesign-identity) codesign_identity="${2:-}"; shift 2 ;;
|
||||||
|
--installer-identity) installer_identity="${2:-}"; shift 2 ;;
|
||||||
|
--provisionprofile) provisionprofile="${2:-}"; shift 2 ;;
|
||||||
|
--entitlements) entitlements="${2:-}"; shift 2 ;;
|
||||||
|
--pkg-out) pkg_out="${2:-}"; shift 2 ;;
|
||||||
|
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
||||||
|
--keychain-password) keychain_password="${2:-}"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$do_run" -eq 0 ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$arm_app" || ! -d "$arm_app" ]]; then
|
||||||
|
echo "Error: missing/invalid --arm-app: $arm_app" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ -z "$x86_app" || ! -d "$x86_app" ]]; then
|
||||||
|
echo "Error: missing/invalid --x86-app: $x86_app" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ -z "$codesign_identity" ]]; then
|
||||||
|
echo "Error: missing --codesign-identity" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ -z "$installer_identity" ]]; then
|
||||||
|
echo "Error: missing --installer-identity" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ -z "$provisionprofile" || ! -f "$provisionprofile" ]]; then
|
||||||
|
echo "Error: missing/invalid --provisionprofile: $provisionprofile" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$entitlements" ]]; then
|
||||||
|
entitlements="${repo_root}/dist/macos/entitlements.mas.plist"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$entitlements" ]]; then
|
||||||
|
echo "Error: entitlements file not found: $entitlements" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$out_dir" ]]; then
|
||||||
|
out_dir="${repo_root}/cmake-build-macos-release-mas-universal"
|
||||||
|
fi
|
||||||
|
mkdir -p "$out_dir"
|
||||||
|
|
||||||
|
universal_app="${out_dir}/strawberry.app"
|
||||||
|
if [[ -e "$universal_app" ]]; then
|
||||||
|
rm -rf "$universal_app"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Repo: $repo_root"
|
||||||
|
echo "==> [$(ts)] arm app: $arm_app"
|
||||||
|
echo "==> [$(ts)] x86 app: $x86_app"
|
||||||
|
echo "==> [$(ts)] out dir: $out_dir"
|
||||||
|
if [[ -n "$bundle_id" ]]; then
|
||||||
|
echo "==> [$(ts)] bundle id (expected): $bundle_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Creating universal app bundle (lipo merge)"
|
||||||
|
"${repo_root}/build_tools/macos/make_universal_app.sh" \
|
||||||
|
--arm-app "$arm_app" \
|
||||||
|
--x86-app "$x86_app" \
|
||||||
|
--out-app "$universal_app" \
|
||||||
|
--clean
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Embedding provisioning profile"
|
||||||
|
cp -f "$provisionprofile" "${universal_app}/Contents/embedded.provisionprofile"
|
||||||
|
|
||||||
|
ensure_keychain_search_list
|
||||||
|
prepare_login_keychain_for_signing "$keychain_password"
|
||||||
|
preflight_identity "codesign" "codesigning" "$codesign_identity"
|
||||||
|
preflight_identity "installer" "basic" "$installer_identity"
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Codesigning universal app (Mac App Store)"
|
||||||
|
codesign_args=( --force --timestamp --options runtime --sign "$codesign_identity" --entitlements "$entitlements" )
|
||||||
|
|
||||||
|
# Clean up any leftover codesign temp files and xattrs.
|
||||||
|
find "$universal_app" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do rm -f "$f" || true; done
|
||||||
|
xattr -dr com.apple.provenance "$universal_app" >/dev/null 2>&1 || true
|
||||||
|
xattr -dr com.apple.quarantine "$universal_app" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Sign nested code first, then frameworks, then the main app bundle.
|
||||||
|
find "$universal_app" -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
|
||||||
|
if /usr/bin/file -b "$f" | grep -q "Mach-O"; then
|
||||||
|
codesign "${codesign_args[@]}" "$f" >/dev/null
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
find "$universal_app" -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
|
||||||
|
|
||||||
|
find "$universal_app/Contents/Frameworks" "$universal_app/Contents/PlugIns" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
|
||||||
|
codesign "${codesign_args[@]}" "$fw" >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
codesign "${codesign_args[@]}" "$universal_app" >/dev/null
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Verifying codesign"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$universal_app"
|
||||||
|
|
||||||
|
if [[ -z "$pkg_out" ]]; then
|
||||||
|
pkg_out="${out_dir}/strawberry-mas-universal.pkg"
|
||||||
|
fi
|
||||||
|
rm -f "$pkg_out" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Building signed .pkg for App Store upload"
|
||||||
|
productbuild \
|
||||||
|
--component "$universal_app" /Applications \
|
||||||
|
--sign "$installer_identity" \
|
||||||
|
"$pkg_out"
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Verifying pkg signature"
|
||||||
|
pkgutil --check-signature "$pkg_out" || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done."
|
||||||
|
echo "Universal app: $universal_app"
|
||||||
|
echo "PKG: $pkg_out"
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
207
build_tools/macos/check_signing_identities.sh
Executable file
207
build_tools/macos/check_signing_identities.sh
Executable file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# macOS signing identity sanity check for:
|
||||||
|
# - Developer ID (outside Mac App Store)
|
||||||
|
# - Mac App Store (Apple Distribution + 3rd Party Mac Developer Installer)
|
||||||
|
|
||||||
|
ts() { date +"%H:%M:%S"; }
|
||||||
|
|
||||||
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||||
|
echo "Error: This script is for macOS only." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Strawberry macOS signing identity check"
|
||||||
|
echo "==> [$(ts)] Host: $(sw_vers -productName 2>/dev/null || true) $(sw_vers -productVersion 2>/dev/null || true)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Keychains searched by 'security' (user)"
|
||||||
|
security list-keychains -d user || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Valid code signing identities (must include private key)"
|
||||||
|
codesigning_out="$(security find-identity -p codesigning -v 2>&1 || true)"
|
||||||
|
echo "$codesigning_out"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Valid installer/pkg identities (must include private key)"
|
||||||
|
basic_out="$(security find-identity -p basic -v 2>&1 || true)"
|
||||||
|
echo "$basic_out"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Note"
|
||||||
|
cat <<'EOF'
|
||||||
|
- Apple uses multiple certificate types. The "basic" identity list can include certificates that are not usable
|
||||||
|
for signing a Mac App Store upload package.
|
||||||
|
- For App Store Connect uploads via .pkg, you typically need an *Installer* identity (e.g. "3rd Party Mac Developer Installer"
|
||||||
|
or "Mac Installer Distribution") and it must have a private key on this Mac.
|
||||||
|
EOF
|
||||||
|
echo
|
||||||
|
|
||||||
|
list_cert_labels() {
|
||||||
|
local query="$1"
|
||||||
|
# Extract "labl" lines like: "labl"<blob>="Apple Distribution: ..."
|
||||||
|
security find-certificate -a -c "$query" 2>/dev/null \
|
||||||
|
| sed -n 's/.*"labl"<blob>="\(.*\)".*/\1/p' \
|
||||||
|
| sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
check_label_in_identities() {
|
||||||
|
local label="$1"
|
||||||
|
local out="$2"
|
||||||
|
if echo "$out" | grep -Fq "$label"; then
|
||||||
|
echo "YES"
|
||||||
|
else
|
||||||
|
echo "NO"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_label_in_installer_identities() {
|
||||||
|
local label="$1"
|
||||||
|
local out="$2"
|
||||||
|
# Only treat as installer-capable if the cert label itself is an installer cert.
|
||||||
|
case "$label" in
|
||||||
|
*Installer*|*installer*) ;;
|
||||||
|
*) echo "NO"; return 0 ;;
|
||||||
|
esac
|
||||||
|
if echo "$out" | grep -Fq "$label"; then
|
||||||
|
echo "YES"
|
||||||
|
else
|
||||||
|
echo "NO"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_section() {
|
||||||
|
local title="$1"
|
||||||
|
shift
|
||||||
|
local queries=("$@")
|
||||||
|
|
||||||
|
echo "==> [$(ts)] ${title}"
|
||||||
|
local any=0
|
||||||
|
|
||||||
|
local q
|
||||||
|
for q in "${queries[@]}"; do
|
||||||
|
local labels
|
||||||
|
labels="$(list_cert_labels "$q" || true)"
|
||||||
|
if [[ -z "$labels" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
any=1
|
||||||
|
while IFS= read -r label; do
|
||||||
|
[[ -z "$label" ]] && continue
|
||||||
|
local in_codesign in_basic
|
||||||
|
in_codesign="$(check_label_in_identities "$label" "$codesigning_out")"
|
||||||
|
in_basic="$(check_label_in_installer_identities "$label" "$basic_out")"
|
||||||
|
printf -- "- %s\n" "$label"
|
||||||
|
printf -- " - codesigning identity: %s\n" "$in_codesign"
|
||||||
|
printf -- " - installer identity: %s\n" "$in_basic"
|
||||||
|
if [[ "$in_codesign" == "NO" && "$in_basic" == "NO" ]]; then
|
||||||
|
printf -- " - note: certificate exists, but it is NOT a usable identity on this Mac (almost always missing private key)\n"
|
||||||
|
fi
|
||||||
|
done <<<"$labels"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$any" -eq 0 ]]; then
|
||||||
|
echo "(no matching certificates found)"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
print_section "Expected for Developer ID (outside Mac App Store)" \
|
||||||
|
"Developer ID Application" \
|
||||||
|
"Developer ID Installer"
|
||||||
|
|
||||||
|
print_section "Expected for Mac App Store submissions" \
|
||||||
|
"Apple Distribution" \
|
||||||
|
"Mac App Distribution" \
|
||||||
|
"3rd Party Mac Developer Application" \
|
||||||
|
"3rd Party Mac Developer Installer" \
|
||||||
|
"Mac Installer Distribution"
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Quick interpretation"
|
||||||
|
cat <<'EOF'
|
||||||
|
- If a certificate label appears above, but both:
|
||||||
|
- codesigning identity: NO
|
||||||
|
- installer identity: NO
|
||||||
|
then the certificate is present but NOT usable for signing on this Mac.
|
||||||
|
The most common cause is: the private key is missing.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
- Open Keychain Access → login → "My Certificates"
|
||||||
|
- Expand the certificate. You must see a private key underneath it.
|
||||||
|
- If there is no private key:
|
||||||
|
- Recreate the certificate on this Mac via Xcode (Accounts → Manage Certificates), OR
|
||||||
|
- Import a .p12 that includes the private key from the machine where it was created.
|
||||||
|
EOF
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Provisioning profiles (Mac App Store builds require one)"
|
||||||
|
prof_dirs=(
|
||||||
|
"${HOME}/Library/Developer/Xcode/UserData/Provisioning Profiles"
|
||||||
|
"${HOME}/Library/MobileDevice/Provisioning Profiles"
|
||||||
|
)
|
||||||
|
any_prof=0
|
||||||
|
for prof_dir in "${prof_dirs[@]}"; do
|
||||||
|
if [[ -d "${prof_dir}" ]]; then
|
||||||
|
any_prof=1
|
||||||
|
echo " ${prof_dir}"
|
||||||
|
ls -la "${prof_dir}" | head -n 20
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ "$any_prof" -eq 0 ]]; then
|
||||||
|
echo "(no provisioning profile directories found in common locations)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Tip: to pick the right MAS profile for a bundle id, run:"
|
||||||
|
echo " ./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id com.dryark.strawberry"
|
||||||
|
\n\necho\n
|
||||||
|
echo "==> [$(ts)] Recommended SHA-1 values to use (avoids ambiguity when names are duplicated)"
|
||||||
|
cat <<'EOF'
|
||||||
|
When you have multiple identities with the same display name, prefer using the SHA-1 hash in scripts:
|
||||||
|
|
||||||
|
--codesign-identity "<SHA1>"
|
||||||
|
--installer-identity "<SHA1>"
|
||||||
|
|
||||||
|
This prevents codesign/productbuild from picking an unexpected identity.
|
||||||
|
EOF
|
||||||
|
echo
|
||||||
|
|
||||||
|
extract_identities() {
|
||||||
|
local policy="$1" # codesigning | basic
|
||||||
|
# Output: SHA1|LABEL
|
||||||
|
security find-identity -p "$policy" -v 2>/dev/null \
|
||||||
|
| sed -n 's/^[[:space:]]*[0-9][0-9]*[)] \([0-9A-F]\{40\}\) "\(.*\)"$/\1|\2/p'
|
||||||
|
}
|
||||||
|
|
||||||
|
print_sha_list() {
|
||||||
|
local title="$1"
|
||||||
|
local policy="$2"
|
||||||
|
local label_match="$3"
|
||||||
|
|
||||||
|
echo "$title"
|
||||||
|
local matches
|
||||||
|
matches="$(extract_identities "$policy" | grep -F "$label_match" || true)"
|
||||||
|
if [[ -z "$matches" ]]; then
|
||||||
|
echo " (none found)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local first=1
|
||||||
|
while IFS='|' read -r sha label; do
|
||||||
|
[[ -z "$sha" || -z "$label" ]] && continue
|
||||||
|
if [[ $first -eq 1 ]]; then
|
||||||
|
echo " recommended: $sha ($label)"
|
||||||
|
first=0
|
||||||
|
else
|
||||||
|
echo " alternative: $sha ($label)"
|
||||||
|
fi
|
||||||
|
done <<<"$matches"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_sha_list "Mac App Store (app signing) [use with --codesign-identity]:" "codesigning" "Apple Distribution:"
|
||||||
|
print_sha_list "Mac App Store (pkg signing) [use with --installer-identity]:" "basic" "3rd Party Mac Developer Installer:"
|
||||||
|
print_sha_list "Developer ID (app signing) [outside App Store]:" "codesigning" "Developer ID Application:"
|
||||||
|
print_sha_list "Developer ID (pkg signing) [outside App Store]:" "basic" "Developer ID Installer:"
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
Encryption Export Compliance Statement (EAR)
|
||||||
|
|
||||||
|
App Name: Strawberry
|
||||||
|
Bundle ID: com.dryark.strawberry
|
||||||
|
Version: 0.0.0
|
||||||
|
Developer: Dry Ark LLC
|
||||||
|
Date: 2026-01-22
|
||||||
|
Contact: support@example.com
|
||||||
|
|
||||||
|
Statement
|
||||||
|
---------
|
||||||
|
This app uses encryption only for standard network communications security (for example, TLS/HTTPS connections to web services).
|
||||||
|
|
||||||
|
The app does not implement proprietary or non-standard encryption algorithms, and it does not ship its own cryptographic library in place of Apple’s operating system encryption.
|
||||||
|
|
||||||
|
The app uses only encryption provided by Apple’s operating system (e.g., Apple-provided TLS stacks accessed through system frameworks used by the app and its dependencies).
|
||||||
|
|
||||||
|
The app is not a VPN client/server, does not provide end-to-end encrypted messaging, and does not provide user-controlled key management or custom cryptographic functionality beyond standard transport security.
|
||||||
|
|
||||||
|
Accordingly, the app’s use of encryption is believed to qualify as exempt under U.S. export regulations for mass-market software using standard OS-provided encryption.
|
||||||
|
|
||||||
|
Reference
|
||||||
|
---------
|
||||||
|
Apple: Complying with Encryption Export Regulations
|
||||||
|
https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations
|
||||||
|
|
||||||
BIN
build_tools/macos/export_compliance/EXPORT_COMPLIANCE.pdf
Normal file
BIN
build_tools/macos/export_compliance/EXPORT_COMPLIANCE.pdf
Normal file
Binary file not shown.
26
build_tools/macos/export_compliance/EXPORT_COMPLIANCE.txt
Normal file
26
build_tools/macos/export_compliance/EXPORT_COMPLIANCE.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
Encryption Export Compliance Statement (EAR)
|
||||||
|
|
||||||
|
App Name: Strawberry
|
||||||
|
Bundle ID: @BUNDLE_ID@
|
||||||
|
Version: @VERSION@
|
||||||
|
Developer: @DEVELOPER@
|
||||||
|
Date: @DATE@
|
||||||
|
Contact: @CONTACT@
|
||||||
|
|
||||||
|
Statement
|
||||||
|
---------
|
||||||
|
This app uses encryption only for standard network communications security (for example, TLS/HTTPS connections to web services).
|
||||||
|
|
||||||
|
The app does not implement proprietary or non-standard encryption algorithms, and it does not ship its own cryptographic library in place of Apple’s operating system encryption.
|
||||||
|
|
||||||
|
The app uses only encryption provided by Apple’s operating system (e.g., Apple-provided TLS stacks accessed through system frameworks used by the app and its dependencies).
|
||||||
|
|
||||||
|
The app is not a VPN client/server, does not provide end-to-end encrypted messaging, and does not provide user-controlled key management or custom cryptographic functionality beyond standard transport security.
|
||||||
|
|
||||||
|
Accordingly, the app’s use of encryption is believed to qualify as exempt under U.S. export regulations for mass-market software using standard OS-provided encryption.
|
||||||
|
|
||||||
|
Reference
|
||||||
|
---------
|
||||||
|
Apple: Complying with Encryption Export Regulations
|
||||||
|
https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations
|
||||||
|
|
||||||
32
build_tools/macos/export_compliance/README.md
Normal file
32
build_tools/macos/export_compliance/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Export compliance (encryption)
|
||||||
|
|
||||||
|
Apple may require an "Export Compliance" statement upload when submitting to the Mac App Store.
|
||||||
|
|
||||||
|
This folder contains:
|
||||||
|
|
||||||
|
- `EXPORT_COMPLIANCE.txt`: a template statement (fill-in placeholders)
|
||||||
|
- `make_pdf.sh`: a helper to fill the template and generate a PDF you can upload
|
||||||
|
|
||||||
|
## Generate the PDF
|
||||||
|
|
||||||
|
From the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_tools/macos/export_compliance/make_pdf.sh \
|
||||||
|
--bundle-id com.dryark.strawberry \
|
||||||
|
--version 1.2.3 \
|
||||||
|
--developer "Dry Ark LLC" \
|
||||||
|
--contact "support@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
|
||||||
|
- `build_tools/macos/export_compliance/EXPORT_COMPLIANCE.filled.txt`
|
||||||
|
- `build_tools/macos/export_compliance/EXPORT_COMPLIANCE.pdf`
|
||||||
|
|
||||||
|
## Important
|
||||||
|
|
||||||
|
This template assumes the app uses **only standard OS-provided encryption** (e.g. TLS/HTTPS via system frameworks) and does **not** ship proprietary or standalone crypto libraries.
|
||||||
|
|
||||||
|
If you bundle your own crypto library (e.g. OpenSSL) or implement custom encryption, you likely need different answers/documentation.
|
||||||
|
|
||||||
96
build_tools/macos/export_compliance/make_pdf.sh
Executable file
96
build_tools/macos/export_compliance/make_pdf.sh
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./build_tools/macos/export_compliance/make_pdf.sh \
|
||||||
|
--bundle-id com.dryark.strawberry \
|
||||||
|
--version 1.2.3 \
|
||||||
|
--developer "Dry Ark LLC" \
|
||||||
|
--contact "support@example.com"
|
||||||
|
|
||||||
|
Outputs (in the same folder as this script):
|
||||||
|
- EXPORT_COMPLIANCE.filled.txt
|
||||||
|
- EXPORT_COMPLIANCE.pdf
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Uses macOS built-in /usr/sbin/cupsfilter to generate the PDF from plain text.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
template="${script_dir}/EXPORT_COMPLIANCE.txt"
|
||||||
|
filled="${script_dir}/EXPORT_COMPLIANCE.filled.txt"
|
||||||
|
pdf="${script_dir}/EXPORT_COMPLIANCE.pdf"
|
||||||
|
tmp_html="${script_dir}/EXPORT_COMPLIANCE.tmp.html"
|
||||||
|
|
||||||
|
bundle_id=""
|
||||||
|
version=""
|
||||||
|
developer=""
|
||||||
|
contact=""
|
||||||
|
date_str="$(date +%Y-%m-%d)"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
||||||
|
--version) version="${2:-}"; shift 2 ;;
|
||||||
|
--developer) developer="${2:-}"; shift 2 ;;
|
||||||
|
--contact) contact="${2:-}"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$bundle_id" || -z "$version" || -z "$developer" || -z "$contact" ]]; then
|
||||||
|
echo "Error: missing required args." >&2
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||||
|
echo "Error: This script is for macOS only (uses textutil)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$template" ]]; then
|
||||||
|
echo "Error: missing template file: $template" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x /usr/sbin/cupsfilter ]]; then
|
||||||
|
echo "Error: /usr/sbin/cupsfilter not found. This should exist on macOS." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
escape_sed_repl() {
|
||||||
|
# Escape characters that are special in sed replacement strings: \ & and delimiter |
|
||||||
|
# bash 3.2 compatible
|
||||||
|
echo "$1" | sed -e 's/[\\&|]/\\&/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle_id_esc="$(escape_sed_repl "$bundle_id")"
|
||||||
|
version_esc="$(escape_sed_repl "$version")"
|
||||||
|
developer_esc="$(escape_sed_repl "$developer")"
|
||||||
|
contact_esc="$(escape_sed_repl "$contact")"
|
||||||
|
date_esc="$(escape_sed_repl "$date_str")"
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e "s|@BUNDLE_ID@|${bundle_id_esc}|g" \
|
||||||
|
-e "s|@VERSION@|${version_esc}|g" \
|
||||||
|
-e "s|@DEVELOPER@|${developer_esc}|g" \
|
||||||
|
-e "s|@CONTACT@|${contact_esc}|g" \
|
||||||
|
-e "s|@DATE@|${date_esc}|g" \
|
||||||
|
"$template" > "$filled"
|
||||||
|
|
||||||
|
rm -f "$pdf" >/dev/null 2>&1 || true
|
||||||
|
rm -f "$tmp_html" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Convert plain text to PDF. cupsfilter writes PDF to stdout.
|
||||||
|
# Suppress noisy DEBUG output from cupsfilter on stderr.
|
||||||
|
/usr/sbin/cupsfilter -i text/plain -m application/pdf "$filled" > "$pdf" 2>/dev/null
|
||||||
|
|
||||||
|
echo "Wrote:"
|
||||||
|
echo " $filled"
|
||||||
|
echo " $pdf"
|
||||||
|
|
||||||
210
build_tools/macos/find_mas_provisioning_profile.py
Normal file
210
build_tools/macos/find_mas_provisioning_profile.py
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import plistlib
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProfileInfo:
|
||||||
|
path: Path
|
||||||
|
uuid: str
|
||||||
|
name: str
|
||||||
|
team_id: str
|
||||||
|
expiration: Optional[dt.datetime]
|
||||||
|
app_id: str
|
||||||
|
platforms: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd: List[str]) -> Tuple[int, bytes, bytes]:
|
||||||
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
out, err = p.communicate()
|
||||||
|
return p.returncode, out, err
|
||||||
|
|
||||||
|
|
||||||
|
def decode_profile_plist_bytes(profile_path: Path) -> Optional[bytes]:
|
||||||
|
# Provisioning profiles are typically CMS/PKCS#7 SignedData blobs whose payload is a plist.
|
||||||
|
# However, some tools store them as plain XML plists already. Also, LibreSSL/OpenSSL behavior
|
||||||
|
# differs: LibreSSL usually requires an explicit '-verify' to emit the embedded content.
|
||||||
|
data = profile_path.read_bytes()
|
||||||
|
|
||||||
|
# Fast path: already a plist (XML).
|
||||||
|
if b"<plist" in data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Decode CMS/PKCS7 to extract embedded plist payload.
|
||||||
|
# Try a small matrix of commands/inform formats for compatibility.
|
||||||
|
candidates: List[List[str]] = []
|
||||||
|
for inform in ("DER", "PEM"):
|
||||||
|
candidates.append(["/usr/bin/openssl", "cms", "-verify", "-noverify", "-inform", inform, "-in", str(profile_path)])
|
||||||
|
candidates.append(["/usr/bin/openssl", "smime", "-verify", "-noverify", "-inform", inform, "-in", str(profile_path)])
|
||||||
|
|
||||||
|
for cmd in candidates:
|
||||||
|
rc, out, _err = run(cmd)
|
||||||
|
if rc == 0 and b"<plist" in out:
|
||||||
|
return out
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_plist(plist_bytes: bytes) -> Dict[str, Any]:
|
||||||
|
return plistlib.loads(plist_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def iso(dt_obj: Optional[dt.datetime]) -> str:
|
||||||
|
if not dt_obj:
|
||||||
|
return "(unknown)"
|
||||||
|
# Force UTC-ish display if tz-aware, otherwise as-is.
|
||||||
|
try:
|
||||||
|
return dt_obj.isoformat().replace("+00:00", "Z")
|
||||||
|
except Exception:
|
||||||
|
return str(dt_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_str(v: Any) -> str:
|
||||||
|
if v is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(v, bytes):
|
||||||
|
try:
|
||||||
|
return v.decode("utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
return repr(v)
|
||||||
|
return str(v)
|
||||||
|
|
||||||
|
|
||||||
|
def profile_info_from_plist(path: Path, p: Dict[str, Any]) -> ProfileInfo:
|
||||||
|
uuid = safe_str(p.get("UUID", "")) or "(unknown)"
|
||||||
|
name = safe_str(p.get("Name", "")) or "(unknown)"
|
||||||
|
team_ids = p.get("TeamIdentifier") or []
|
||||||
|
team_id = safe_str(team_ids[0]) if isinstance(team_ids, list) and team_ids else ""
|
||||||
|
if not team_id:
|
||||||
|
prefixes = p.get("ApplicationIdentifierPrefix") or []
|
||||||
|
team_id = safe_str(prefixes[0]) if isinstance(prefixes, list) and prefixes else "(unknown)"
|
||||||
|
exp = p.get("ExpirationDate")
|
||||||
|
expiration = exp if isinstance(exp, dt.datetime) else None
|
||||||
|
ent = p.get("Entitlements") or {}
|
||||||
|
app_id = safe_str(ent.get("application-identifier") or ent.get("com.apple.application-identifier") or "") or "(unknown)"
|
||||||
|
platforms = p.get("Platform") or []
|
||||||
|
if isinstance(platforms, str):
|
||||||
|
platforms = [platforms]
|
||||||
|
platforms = [safe_str(x) for x in platforms if x is not None]
|
||||||
|
return ProfileInfo(path=path, uuid=uuid, name=name, team_id=team_id or "(unknown)", expiration=expiration, app_id=app_id, platforms=platforms)
|
||||||
|
|
||||||
|
|
||||||
|
def score(profile: ProfileInfo, bundle_id: str, now: dt.datetime) -> Tuple[int, str]:
|
||||||
|
# Prefer non-expired.
|
||||||
|
if profile.expiration and profile.expiration < now:
|
||||||
|
return (-1, "expired")
|
||||||
|
|
||||||
|
score = 0
|
||||||
|
reason = []
|
||||||
|
|
||||||
|
# Prefer exact app id match TEAMID.bundle_id
|
||||||
|
if profile.team_id != "(unknown)" and profile.app_id != "(unknown)":
|
||||||
|
exact = f"{profile.team_id}.{bundle_id}"
|
||||||
|
if profile.app_id == exact:
|
||||||
|
score += 100
|
||||||
|
reason.append(f"exact {profile.app_id}")
|
||||||
|
elif profile.app_id.endswith(f".{bundle_id}"):
|
||||||
|
score += 60
|
||||||
|
reason.append(f"endswith {profile.app_id}")
|
||||||
|
elif "*" in profile.app_id and profile.app_id.startswith(f"{profile.team_id}."):
|
||||||
|
score += 40
|
||||||
|
reason.append(f"wildcard {profile.app_id}")
|
||||||
|
|
||||||
|
# Heuristic: name suggests MAS.
|
||||||
|
n = profile.name.lower()
|
||||||
|
if "mac app store" in n or "app store" in n or "appstore" in n:
|
||||||
|
score += 5
|
||||||
|
reason.append("name looks like MAS")
|
||||||
|
|
||||||
|
# Prefer macOS platform if present.
|
||||||
|
plats = [p.lower() for p in profile.platforms]
|
||||||
|
if any("macos" in p for p in plats):
|
||||||
|
score += 2
|
||||||
|
reason.append("platform macos")
|
||||||
|
|
||||||
|
return (score, ", ".join(reason) if reason else "")
|
||||||
|
|
||||||
|
|
||||||
|
def find_profiles() -> List[Path]:
|
||||||
|
dirs = [
|
||||||
|
Path.home() / "Library" / "Developer" / "Xcode" / "UserData" / "Provisioning Profiles",
|
||||||
|
Path.home() / "Library" / "MobileDevice" / "Provisioning Profiles",
|
||||||
|
]
|
||||||
|
out: List[Path] = []
|
||||||
|
for d in dirs:
|
||||||
|
if not d.is_dir():
|
||||||
|
continue
|
||||||
|
for p in d.iterdir():
|
||||||
|
if p.is_file() and (p.name.endswith(".provisionprofile") or p.name.endswith(".mobileprovision")):
|
||||||
|
out.append(p)
|
||||||
|
return sorted(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--bundle-id", required=True)
|
||||||
|
args = ap.parse_args()
|
||||||
|
bundle_id = args.bundle_id
|
||||||
|
|
||||||
|
if not Path("/usr/bin/openssl").exists():
|
||||||
|
print("Error: /usr/bin/openssl not found.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
candidates = find_profiles()
|
||||||
|
if not candidates:
|
||||||
|
print("No provisioning profiles found in common locations.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"Scanning {len(candidates)} provisioning profile(s) for bundle id: {bundle_id}")
|
||||||
|
print()
|
||||||
|
print(f"{'No.':<4} {'UUID':<36} {'TeamID':<10} {'Expires':<25} {'AppID':<45} Path")
|
||||||
|
print(f"{'-'*4} {'-'*36} {'-'*10} {'-'*25} {'-'*45} ----")
|
||||||
|
|
||||||
|
infos: List[ProfileInfo] = []
|
||||||
|
for i, p in enumerate(candidates, start=1):
|
||||||
|
plist_bytes = decode_profile_plist_bytes(p)
|
||||||
|
if not plist_bytes:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
pl = parse_plist(plist_bytes)
|
||||||
|
info = profile_info_from_plist(p, pl)
|
||||||
|
infos.append(info)
|
||||||
|
print(f"{i:<4} {info.uuid:<36} {info.team_id:<10} {iso(info.expiration):<25} {info.app_id:<45} {info.path}")
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not infos:
|
||||||
|
print("\nCould not decode any provisioning profiles with openssl cms.", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
now = dt.datetime.now(dt.timezone.utc)
|
||||||
|
best: Optional[Tuple[int, str, ProfileInfo]] = None
|
||||||
|
for info in infos:
|
||||||
|
sc, why = score(info, bundle_id, now)
|
||||||
|
if best is None or sc > best[0]:
|
||||||
|
best = (sc, why, info)
|
||||||
|
|
||||||
|
print()
|
||||||
|
if best is None or best[0] <= 0:
|
||||||
|
print(f"Could not confidently auto-select a profile for {bundle_id}.", file=sys.stderr)
|
||||||
|
print("Pick the profile whose AppID is TEAMID.<bundle-id> and is a macOS Mac App Store profile.", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
_, why, info = best
|
||||||
|
print("Recommended profile:")
|
||||||
|
print(f" {info.path}")
|
||||||
|
print(f" reason: {why}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
||||||
160
build_tools/macos/find_mas_provisioning_profile.sh
Executable file
160
build_tools/macos/find_mas_provisioning_profile.sh
Executable file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ts() { date +"%H:%M:%S"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id com.dryark.strawberry
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
- Scans common macOS provisioning profile locations (new Xcode + legacy)
|
||||||
|
- Uses Apple's `security cms -D` to decode each *.provisionprofile into a plist
|
||||||
|
- Prints a readable table and recommends a best match for the given bundle id
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- A provisioning profile is required for Mac App Store signing.
|
||||||
|
- This script only helps you *find* the right profile file.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||||
|
echo "Error: This script is for macOS only." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
bundle_id=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$bundle_id" ]]; then
|
||||||
|
echo "Error: missing --bundle-id" >&2
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v security >/dev/null 2>&1; then
|
||||||
|
echo "Error: 'security' not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
plistbuddy_print() {
|
||||||
|
local keypath="$1"
|
||||||
|
local plist="$2"
|
||||||
|
/usr/libexec/PlistBuddy -c "Print :${keypath}" "$plist" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
plutil_extract() {
|
||||||
|
local keypath="$1"
|
||||||
|
local plist="$2"
|
||||||
|
/usr/bin/plutil -extract "$keypath" raw -o - "$plist" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
find_profiles_in_dir() {
|
||||||
|
local dir="$1"
|
||||||
|
if [[ -d "$dir" ]]; then
|
||||||
|
find "$dir" -maxdepth 1 -type f \( -name "*.provisionprofile" -o -name "*.mobileprovision" \) 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
declare -a candidates
|
||||||
|
candidates=()
|
||||||
|
while IFS= read -r f; do candidates+=("$f"); done < <(find_profiles_in_dir "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles")
|
||||||
|
while IFS= read -r f; do candidates+=("$f"); done < <(find_profiles_in_dir "$HOME/Library/MobileDevice/Provisioning Profiles")
|
||||||
|
|
||||||
|
if [[ ${#candidates[@]} -eq 0 ]]; then
|
||||||
|
echo "==> [$(ts)] No provisioning profiles found in common locations."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Scanning ${#candidates[@]} provisioning profile(s) for bundle id: ${bundle_id}"
|
||||||
|
echo
|
||||||
|
printf "%-4s %-36s %-10s %-25s %-45s %s\n" "No." "UUID" "TeamID" "Expires" "AppID" "Path"
|
||||||
|
printf "%s\n" "---- ------------------------------------ ---------- ------------------------- --------------------------------------------- ----"
|
||||||
|
|
||||||
|
best_score=-1
|
||||||
|
best_path=""
|
||||||
|
best_reason=""
|
||||||
|
|
||||||
|
idx=0
|
||||||
|
for f in "${candidates[@]}"; do
|
||||||
|
idx=$((idx + 1))
|
||||||
|
|
||||||
|
tmp="$(mktemp -t strawberry-profile.XXXXXX.plist)"
|
||||||
|
if ! security cms -D -i "$f" >"$tmp" 2>/dev/null; then
|
||||||
|
rm -f "$tmp" >/dev/null 2>&1 || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
uuid="$(plutil_extract UUID "$tmp")"
|
||||||
|
name="$(plutil_extract Name "$tmp")"
|
||||||
|
teamid="$(plutil_extract 'TeamIdentifier.0' "$tmp")"
|
||||||
|
if [[ -z "$teamid" ]]; then
|
||||||
|
teamid="$(plutil_extract 'ApplicationIdentifierPrefix.0' "$tmp")"
|
||||||
|
fi
|
||||||
|
exp="$(plutil_extract ExpirationDate "$tmp")"
|
||||||
|
|
||||||
|
# App identifier lives under Entitlements; use PlistBuddy because some key names contain dots.
|
||||||
|
appid="$(plistbuddy_print 'Entitlements:application-identifier' "$tmp")"
|
||||||
|
if [[ -z "$appid" ]]; then
|
||||||
|
appid="$(plistbuddy_print 'Entitlements:com.apple.application-identifier' "$tmp")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$tmp" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
[[ -z "$uuid" ]] && uuid="(unknown)"
|
||||||
|
[[ -z "$teamid" ]] && teamid="(unknown)"
|
||||||
|
[[ -z "$exp" ]] && exp="(unknown)"
|
||||||
|
[[ -z "$appid" ]] && appid="(unknown)"
|
||||||
|
|
||||||
|
printf "%-4s %-36s %-10s %-25s %-45s %s\n" "$idx" "$uuid" "$teamid" "$exp" "$appid" "$f"
|
||||||
|
|
||||||
|
score=0
|
||||||
|
reason=""
|
||||||
|
|
||||||
|
if [[ "$appid" != "(unknown)" && "$teamid" != "(unknown)" ]]; then
|
||||||
|
if [[ "$appid" == "${teamid}.${bundle_id}" ]]; then
|
||||||
|
score=100
|
||||||
|
reason="exact match (${appid})"
|
||||||
|
elif [[ "$appid" == *".${bundle_id}" ]]; then
|
||||||
|
score=50
|
||||||
|
reason="endswith match (${appid})"
|
||||||
|
elif [[ "$appid" == "${teamid}."* && "$appid" == *"*"* ]]; then
|
||||||
|
score=40
|
||||||
|
reason="wildcard match (${appid})"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$score" -gt 0 && -n "$name" ]]; then
|
||||||
|
case "$name" in
|
||||||
|
*Mac\ App\ Store*|*App\ Store*|*appstore*|*AppStore*)
|
||||||
|
score=$((score + 5))
|
||||||
|
reason="${reason}, name looks like MAS"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$score" -gt "$best_score" ]]; then
|
||||||
|
best_score="$score"
|
||||||
|
best_path="$f"
|
||||||
|
best_reason="$reason"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [[ "$best_score" -le 0 ]]; then
|
||||||
|
echo "==> [$(ts)] Could not confidently auto-select a profile for ${bundle_id}."
|
||||||
|
echo "Pick the profile whose AppID is TEAMID.${bundle_id} and is a macOS Mac App Store profile."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Recommended profile:"
|
||||||
|
echo " $best_path"
|
||||||
|
echo " reason: $best_reason"
|
||||||
|
|
||||||
141
build_tools/macos/install_brew_deps.sh
Executable file
141
build_tools/macos/install_brew_deps.sh
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# Brewfile support (`brew bundle`) is built into modern Homebrew. The historical
|
||||||
|
# tap `homebrew/bundle` has been deprecated and may be empty on newer Homebrew.
|
||||||
|
# If `brew bundle` is missing, the fix is to update Homebrew itself.
|
||||||
|
if ! brew bundle --help >/dev/null 2>&1; then
|
||||||
|
run_with_heartbeat "Updating Homebrew (required for 'brew bundle')" bash -lc "brew update"
|
||||||
|
if ! brew bundle --help >/dev/null 2>&1; then
|
||||||
|
echo "Error: This Homebrew installation does not provide 'brew bundle'." >&2
|
||||||
|
echo "Update Homebrew (e.g. 'brew update') or reinstall Homebrew, then re-run this script." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
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"
|
||||||
|
|
||||||
|
echo "==> [$(date +\"%H:%M:%S\")] strawberry/local tap repo: $tap_repo"
|
||||||
|
echo " tap HEAD: $(git rev-parse --short HEAD)"
|
||||||
|
echo " origin: $(git remote get-url origin)"
|
||||||
|
# If the source repo is a git repo, also print its HEAD for debugging.
|
||||||
|
if git -C "'"$repo_root"'" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
echo " src HEAD: $(git -C "'"$repo_root"'" rev-parse --short HEAD)"
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
for f in kdsingleapplication-qt6 qtsparkle-qt6 sparkle-framework libgpod macdeploycheck; do
|
||||||
|
if ! info_out="$(brew info "strawberry/local/${f}" 2>&1 >/dev/null)"; then
|
||||||
|
echo "Error: Unable to load formula strawberry/local/${f} from the tapped repo (brew info failed)." >&2
|
||||||
|
echo "Details (brew info):" >&2
|
||||||
|
echo "$info_out" >&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
|
||||||
|
|
||||||
170
build_tools/macos/make_universal_app.sh
Normal file
170
build_tools/macos/make_universal_app.sh
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Create a universal (arm64+x86_64) .app by merging two already-deployed app bundles
|
||||||
|
# that have identical layouts, one built on Apple Silicon and one built on Intel.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./build_tools/macos/make_universal_app.sh \
|
||||||
|
# --arm-app /path/to/arm64/strawberry.app \
|
||||||
|
# --x86-app /path/to/x86_64/strawberry.app \
|
||||||
|
# --out-app /path/to/output/strawberry.app \
|
||||||
|
# [--clean]
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# - Do NOT sign the per-arch apps first; signatures will be invalidated by lipo anyway.
|
||||||
|
# - Both inputs must be the same app version/config with the same enabled features,
|
||||||
|
# so the file lists match.
|
||||||
|
|
||||||
|
ts() { date +"%H:%M:%S"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./build_tools/macos/make_universal_app.sh --arm-app <path> --x86-app <path> --out-app <path> [--clean]
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
- Copies the arm64 app to --out-app
|
||||||
|
- For every Mach-O file in the copied app, finds the corresponding file in the x86_64 app
|
||||||
|
- Uses lipo to combine the two slices into a universal binary at the same relative path
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--arm-app <path> Path to arm64 Strawberry.app (built+deployed on Apple Silicon)
|
||||||
|
--x86-app <path> Path to x86_64 Strawberry.app (built+deployed on Intel)
|
||||||
|
--out-app <path> Output path for universal Strawberry.app
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--clean Delete --out-app if it already exists
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||||
|
echo "Error: This script is for macOS only." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
arm_app=""
|
||||||
|
x86_app=""
|
||||||
|
out_app=""
|
||||||
|
do_clean=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--arm-app) arm_app="${2:-}"; shift 2 ;;
|
||||||
|
--x86-app) x86_app="${2:-}"; shift 2 ;;
|
||||||
|
--out-app) out_app="${2:-}"; shift 2 ;;
|
||||||
|
--clean) do_clean=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$arm_app" || -z "$x86_app" || -z "$out_app" ]]; then
|
||||||
|
echo "Error: missing required args." >&2
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ ! -d "$arm_app" ]]; then
|
||||||
|
echo "Error: --arm-app not found: $arm_app" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ ! -d "$x86_app" ]]; then
|
||||||
|
echo "Error: --x86-app not found: $x86_app" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
for cmd in /usr/bin/file /usr/bin/lipo /usr/bin/ditto; do
|
||||||
|
if [[ ! -x "$cmd" ]]; then
|
||||||
|
echo "Error: required tool not found: $cmd" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
out_parent="$(cd -- "$(dirname -- "$out_app")" && pwd)"
|
||||||
|
out_name="$(basename -- "$out_app")"
|
||||||
|
out_app="${out_parent}/${out_name}"
|
||||||
|
|
||||||
|
if [[ -e "$out_app" && "$do_clean" -eq 1 ]]; then
|
||||||
|
echo "==> [$(ts)] Removing existing output app: $out_app"
|
||||||
|
rm -rf "$out_app"
|
||||||
|
fi
|
||||||
|
if [[ -e "$out_app" ]]; then
|
||||||
|
echo "Error: output already exists: $out_app (use --clean to overwrite)" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Copying arm64 app to output"
|
||||||
|
/usr/bin/ditto "$arm_app" "$out_app"
|
||||||
|
|
||||||
|
# Remove any existing signatures in the copied app; we'll re-sign after creating universal binaries.
|
||||||
|
echo "==> [$(ts)] Removing existing code signature metadata (will be re-signed later)"
|
||||||
|
find "$out_app" -type d -name "_CodeSignature" -print0 2>/dev/null | while IFS= read -r -d '' d; do
|
||||||
|
rm -rf "$d" || true
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Merging Mach-O files with lipo"
|
||||||
|
|
||||||
|
merged=0
|
||||||
|
skipped=0
|
||||||
|
|
||||||
|
# Traverse output app and lipo-merge any Mach-O file with its counterpart in the x86 app.
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
# Only operate on regular files.
|
||||||
|
if [[ ! -f "$f" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
ft="$(/usr/bin/file -b "$f" 2>/dev/null || true)"
|
||||||
|
if [[ "$ft" != *"Mach-O"* ]]; then
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
rel="${f#"$out_app"/}"
|
||||||
|
other="${x86_app}/${rel}"
|
||||||
|
if [[ ! -f "$other" ]]; then
|
||||||
|
echo "Error: missing matching file in x86 app for:" >&2
|
||||||
|
echo " $rel" >&2
|
||||||
|
echo "Expected at:" >&2
|
||||||
|
echo " $other" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
other_ft="$(/usr/bin/file -b "$other" 2>/dev/null || true)"
|
||||||
|
if [[ "$other_ft" != *"Mach-O"* ]]; then
|
||||||
|
echo "Error: file is Mach-O in arm app but not Mach-O in x86 app:" >&2
|
||||||
|
echo " $rel" >&2
|
||||||
|
echo "arm64: $ft" >&2
|
||||||
|
echo "x86_64: $other_ft" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate architectures.
|
||||||
|
arm_archs="$(/usr/bin/lipo -archs "$f" 2>/dev/null || true)"
|
||||||
|
x86_archs="$(/usr/bin/lipo -archs "$other" 2>/dev/null || true)"
|
||||||
|
if [[ "$arm_archs" != *"arm64"* ]]; then
|
||||||
|
echo "Error: expected arm64 slice in arm app file:" >&2
|
||||||
|
echo " $rel" >&2
|
||||||
|
echo " archs: $arm_archs" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$x86_archs" != *"x86_64"* ]]; then
|
||||||
|
echo "Error: expected x86_64 slice in x86 app file:" >&2
|
||||||
|
echo " $rel" >&2
|
||||||
|
echo " archs: $x86_archs" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp="$(mktemp -t strawberry-universal.XXXXXX)"
|
||||||
|
/usr/bin/lipo -create "$f" "$other" -output "$tmp"
|
||||||
|
chmod --reference="$f" "$tmp" 2>/dev/null || true
|
||||||
|
mv -f "$tmp" "$f"
|
||||||
|
merged=$((merged + 1))
|
||||||
|
done < <(find "$out_app" -type f -print0 2>/dev/null)
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Done"
|
||||||
|
echo "Merged Mach-O files: $merged"
|
||||||
|
echo "Non-Mach-O files skipped: $skipped"
|
||||||
|
echo "Output app:"
|
||||||
|
echo " $out_app"
|
||||||
|
|
||||||
70
build_tools/macos/print_mas_build_cmd.sh
Executable file
70
build_tools/macos/print_mas_build_cmd.sh
Executable file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ts() { date +"%H:%M:%S"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
./build_tools/macos/print_mas_build_cmd.sh [--bundle-id com.dryark.strawberry] [--profile <path>]
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
- Tries to auto-pick a provisioning profile for the bundle id
|
||||||
|
- Prints an exact build command you can copy/paste for build_mas_pkg.sh
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- This helper intentionally does NOT try to auto-pick signing identities by parsing Apple tool output.
|
||||||
|
Use SHA-1 identities from:
|
||||||
|
./build_tools/macos/check_signing_identities.sh
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||||
|
echo "Error: This script is for macOS only." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
||||||
|
|
||||||
|
bundle_id="com.dryark.strawberry"
|
||||||
|
profile_path=""
|
||||||
|
codesign_identity=""
|
||||||
|
installer_identity=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
||||||
|
--profile) profile_path="${2:-}"; shift 2 ;;
|
||||||
|
--codesign-identity) codesign_identity="${2:-}"; shift 2 ;;
|
||||||
|
--installer-identity) installer_identity="${2:-}"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$profile_path" ]]; then
|
||||||
|
# Attempt to auto-select profile using the finder script.
|
||||||
|
finder="${repo_root}/build_tools/macos/find_mas_provisioning_profile.sh"
|
||||||
|
if [[ -x "$finder" ]]; then
|
||||||
|
out="$("$finder" --bundle-id "$bundle_id" 2>/dev/null || true)"
|
||||||
|
# Parse the line after "Recommended profile:"
|
||||||
|
profile_path="$(printf '%s\n' "$out" | awk 'found{print $1; exit} /^Recommended profile:/{found=1} found && $0 ~ /^ \\// {print $1; exit}' | sed 's/^[[:space:]]*//')"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> [$(ts)] Recommended build command:"
|
||||||
|
echo
|
||||||
|
echo "./build_tools/macos/build_mas_pkg.sh --run --release --clean \\"
|
||||||
|
echo " --codesign-identity \"${codesign_identity:-<SHA1 from check_signing_identities.sh>}\" \\"
|
||||||
|
echo " --installer-identity \"${installer_identity:-<SHA1 from check_signing_identities.sh>}\" \\"
|
||||||
|
if [[ -n "$profile_path" ]]; then
|
||||||
|
echo " --provisionprofile \"${profile_path}\""
|
||||||
|
else
|
||||||
|
echo " --provisionprofile \"</path/to/profile.provisionprofile>\""
|
||||||
|
echo
|
||||||
|
echo "Note: could not auto-pick a provisioning profile for bundle id '${bundle_id}'."
|
||||||
|
echo "Run:"
|
||||||
|
echo " ./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id ${bundle_id}"
|
||||||
|
fi
|
||||||
|
|
||||||
@@ -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)
|
if(MACDEPLOYQT_EXECUTABLE)
|
||||||
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
||||||
else()
|
else()
|
||||||
message(WARNING "Missing macdeployqt executable.")
|
message(WARNING "Missing macdeployqt executable.")
|
||||||
endif()
|
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)
|
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||||
message(STATUS "Found macdeploycheck: ${MACDEPLOYCHECK_EXECUTABLE}")
|
message(STATUS "Found macdeploycheck: ${MACDEPLOYCHECK_EXECUTABLE}")
|
||||||
else()
|
else()
|
||||||
message(WARNING "Missing macdeploycheck executable.")
|
message(STATUS "macdeploycheck not found (optional): 'deploycheck' target will be unavailable.")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg REQUIRED)
|
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg)
|
||||||
if(CREATEDMG_EXECUTABLE)
|
if(CREATEDMG_EXECUTABLE)
|
||||||
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
||||||
else()
|
else()
|
||||||
message(WARNING "Missing create-dmg executable.")
|
message(WARNING "Missing create-dmg executable.")
|
||||||
endif()
|
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(MACDEPLOYQT_EXECUTABLE)
|
||||||
|
|
||||||
if(APPLE_DEVELOPER_ID)
|
# Note: We intentionally do NOT codesign during the CMake 'deploy'/'dmg' targets.
|
||||||
set(MACDEPLOYQT_CODESIGN -codesign=${APPLE_DEVELOPER_ID})
|
# macdeployqt can optionally sign, but passing identities safely through Ninja's /bin/sh wrapper is brittle.
|
||||||
set(CREATEDMG_CODESIGN --codesign ${APPLE_DEVELOPER_ID})
|
# This repo's signing/notarization pipeline is handled in build_tools/macos/build_sign_notarize.sh instead.
|
||||||
endif()
|
|
||||||
if(CREATEDMG_SKIP_JENKINS)
|
if(CREATEDMG_SKIP_JENKINS)
|
||||||
set(CREATEDMG_SKIP_JENKINS_ARG "--skip-jenkins")
|
set(CREATEDMG_SKIP_JENKINS_ARG "--skip-jenkins")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_custom_target(deploy
|
set(_deploy_commands
|
||||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/{Frameworks,Resources}
|
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_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 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)
|
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||||
add_custom_target(deploycheck
|
add_custom_target(deploycheck
|
||||||
COMMAND ${MACDEPLOYCHECK_EXECUTABLE} strawberry.app
|
COMMAND ${MACDEPLOYCHECK_EXECUTABLE} strawberry.app
|
||||||
@@ -45,8 +106,9 @@ if(MACDEPLOYQT_EXECUTABLE)
|
|||||||
endif()
|
endif()
|
||||||
if(CREATEDMG_EXECUTABLE)
|
if(CREATEDMG_EXECUTABLE)
|
||||||
add_custom_target(dmg
|
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}
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
DEPENDS deploy
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
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_willbuild "")
|
||||||
set(summary_willnotbuild "")
|
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)
|
macro(optional_component_summary_add name test)
|
||||||
if (${test})
|
if (${test})
|
||||||
list(APPEND summary_willbuild ${name})
|
list(APPEND summary_willbuild ${name})
|
||||||
@@ -80,8 +97,13 @@ function(optional_component name default description)
|
|||||||
set(text "${description} (missing ${deplist_text})")
|
set(text "${description} (missing ${deplist_text})")
|
||||||
|
|
||||||
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
|
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
|
||||||
|
if(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL)
|
||||||
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
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()
|
else()
|
||||||
set(${have_variable} ON PARENT_SCOPE)
|
set(${have_variable} ON PARENT_SCOPE)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||||
set(STRAWBERRY_VERSION_MINOR 2)
|
set(STRAWBERRY_VERSION_MINOR 2)
|
||||||
set(STRAWBERRY_VERSION_PATCH 9)
|
set(STRAWBERRY_VERSION_PATCH 17)
|
||||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||||
|
|
||||||
set(INCLUDE_GIT_REVISION ON)
|
set(INCLUDE_GIT_REVISION ON)
|
||||||
|
|||||||
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-18.sql</file>
|
||||||
<file>schema/schema-19.sql</file>
|
<file>schema/schema-19.sql</file>
|
||||||
<file>schema/schema-20.sql</file>
|
<file>schema/schema-20.sql</file>
|
||||||
|
<file>schema/schema-21.sql</file>
|
||||||
<file>schema/device-schema.sql</file>
|
<file>schema/device-schema.sql</file>
|
||||||
<file>style/strawberry.css</file>
|
<file>style/strawberry.css</file>
|
||||||
<file>style/smartplaylistsearchterm.css</file>
|
<file>style/smartplaylistsearchterm.css</file>
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ CREATE TABLE device_%deviceid_subdirectories (
|
|||||||
CREATE TABLE device_%deviceid_songs (
|
CREATE TABLE device_%deviceid_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -22,7 +26,9 @@ CREATE TABLE device_%deviceid_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -86,7 +92,11 @@ CREATE TABLE device_%deviceid_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -94,4 +104,4 @@ CREATE INDEX idx_device_%deviceid_songs_album ON device_%deviceid_songs (album);
|
|||||||
|
|
||||||
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
|
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
|
||||||
|
|
||||||
UPDATE devices SET schema_version=5 WHERE ROWID=%deviceid;
|
UPDATE devices SET schema_version=6 WHERE ROWID=%deviceid;
|
||||||
|
|||||||
43
data/schema/schema-21.sql
Normal file
43
data/schema/schema-21.sql
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_albumartistsort;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_albumsort;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_artistsort;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_composersort;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_performersort;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_titlesort;
|
||||||
|
|
||||||
|
ALTER TABLE %allsongstables ADD COLUMN albumartistsort TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE %allsongstables ADD COLUMN albumsort TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE %allsongstables ADD COLUMN artistsort TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE %allsongstables ADD COLUMN composersort TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE %allsongstables ADD COLUMN performersort TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE %allsongstables ADD COLUMN titlesort TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE %allsongstables ADD COLUMN bpm REAL;
|
||||||
|
|
||||||
|
ALTER TABLE %allsongstables ADD COLUMN mood TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE %allsongstables ADD COLUMN initial_key TEXT;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
|
||||||
|
|
||||||
|
UPDATE schema_version SET version=21;
|
||||||
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
|||||||
|
|
||||||
DELETE FROM schema_version;
|
DELETE FROM schema_version;
|
||||||
|
|
||||||
INSERT INTO schema_version (version) VALUES (20);
|
INSERT INTO schema_version (version) VALUES (21);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS directories (
|
CREATE TABLE IF NOT EXISTS directories (
|
||||||
path TEXT NOT NULL,
|
path TEXT NOT NULL,
|
||||||
@@ -20,9 +20,13 @@ CREATE TABLE IF NOT EXISTS subdirectories (
|
|||||||
CREATE TABLE IF NOT EXISTS songs (
|
CREATE TABLE IF NOT EXISTS songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -30,7 +34,9 @@ CREATE TABLE IF NOT EXISTS songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -94,16 +100,24 @@ CREATE TABLE IF NOT EXISTS songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS subsonic_songs (
|
CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -111,7 +125,9 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -175,16 +191,24 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -192,7 +216,9 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -256,16 +282,24 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -273,7 +307,9 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -337,16 +373,24 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tidal_songs (
|
CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -354,7 +398,9 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -418,16 +464,24 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -435,7 +489,9 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -499,16 +555,24 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -516,7 +580,9 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -580,16 +646,24 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS spotify_songs (
|
CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -597,7 +671,9 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -661,16 +737,24 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -678,7 +762,9 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -742,16 +828,24 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -759,7 +853,9 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -823,16 +919,24 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qobuz_songs (
|
CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -840,7 +944,9 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -904,7 +1010,11 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -931,9 +1041,13 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
|||||||
playlist_url TEXT,
|
playlist_url TEXT,
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
|
titlesort TEXT,
|
||||||
album TEXT,
|
album TEXT,
|
||||||
|
albumsort TEXT,
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
|
artistsort TEXT,
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
|
albumartistsort TEXT,
|
||||||
track INTEGER,
|
track INTEGER,
|
||||||
disc INTEGER,
|
disc INTEGER,
|
||||||
year INTEGER,
|
year INTEGER,
|
||||||
@@ -941,7 +1055,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER DEFAULT 0,
|
compilation INTEGER DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
|
composersort TEXT,
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
|
performersort TEXT,
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -1005,7 +1121,11 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL
|
ebur128_loudness_range_lu REAL,
|
||||||
|
|
||||||
|
bpm REAL,
|
||||||
|
mood TEXT,
|
||||||
|
initial_key TEXT
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1032,10 +1152,22 @@ CREATE INDEX IF NOT EXISTS idx_comp_artist ON songs (compilation_effective, arti
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_albumartist ON songs (albumartist);
|
CREATE INDEX IF NOT EXISTS idx_albumartist ON songs (albumartist);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_artist ON songs (artist);
|
CREATE INDEX IF NOT EXISTS idx_artist ON songs (artist);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
|
CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
|
CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
|
||||||
|
|
||||||
CREATE VIEW IF NOT EXISTS duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;
|
CREATE VIEW IF NOT EXISTS duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;
|
||||||
|
|||||||
4
debian/control
vendored
4
debian/control
vendored
@@ -60,11 +60,11 @@ Description: music player and music collection organizer
|
|||||||
- Edit tags on audio files
|
- Edit tags on audio files
|
||||||
- Automatically retrieve tags from MusicBrainz
|
- Automatically retrieve tags from MusicBrainz
|
||||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
- Lyrics from multiple sources
|
||||||
- Audio analyzer
|
- Audio analyzer
|
||||||
- Audio equalizer
|
- Audio equalizer
|
||||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
- Scrobbler with support for Last.fm and ListenBrainz
|
||||||
- Streaming support for Subsonic-compatible servers
|
- Streaming support for Subsonic-compatible servers
|
||||||
- Unofficial streaming support for Tidal and Qobuz
|
- Unofficial streaming support for Tidal and Qobuz
|
||||||
.
|
.
|
||||||
|
|||||||
22
dist/CMakeLists.txt
vendored
22
dist/CMakeLists.txt
vendored
@@ -9,7 +9,27 @@ if(APPLE)
|
|||||||
else()
|
else()
|
||||||
set(LSMinimumSystemVersion 12.0)
|
set(LSMinimumSystemVersion 12.0)
|
||||||
endif()
|
endif()
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
|
||||||
|
if(BUILD_FOR_MAC_APP_STORE)
|
||||||
|
# MAS builds must not embed Sparkle update configuration in Info.plist.
|
||||||
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.mas.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
||||||
|
else()
|
||||||
|
# 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()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
|||||||
253
dist/macos/Info.mas.plist.in
vendored
Normal file
253
dist/macos/Info.mas.plist.in
vendored
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string>NSApplication</string>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>English</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>strawberry</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>Strawberry ${STRAWBERRY_VERSION_DISPLAY}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>strawberry.icns</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>@MACOS_BUNDLE_ID@</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleLongVersionString</key>
|
||||||
|
<string>${STRAWBERRY_VERSION_DISPLAY}</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Strawberry</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>${STRAWBERRY_VERSION_DISPLAY}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>${STRAWBERRY_VERSION_PACKAGE}</string>
|
||||||
|
<key>CSResourcesFileMapped</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSRequiresCarbon</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.music</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>@LSMinimumSystemVersion@</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>@MACOS_BUNDLE_ID@</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>tidal</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeOSTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>****</string>
|
||||||
|
<string>fold</string>
|
||||||
|
<string>disk</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>xspf</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>Generic.icns</string>
|
||||||
|
<key>CFBundleTypeMIMETypes</key>
|
||||||
|
<array>
|
||||||
|
<string>application/xspf+xml</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>XSPF Playlist</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>wav</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeMIMETypes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio/x-wav</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>WAVE Audio File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>pls</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>pls.icns</string>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Shoutcast playlist</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>m3u</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>m3u.icns</string>
|
||||||
|
<key>CFBundleTypeMIMETypes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio/x-mpegurl</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Playlist file</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>aac</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>mpeg4.icns</string>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>AAC file</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>ogg</string>
|
||||||
|
<string>ogx</string>
|
||||||
|
<string>ogm</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>ogg.icns</string>
|
||||||
|
<key>CFBundleTypeMIMETypes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio/ogg</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Ogg Vorbis File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>oga</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>ogg.icns</string>
|
||||||
|
<key>CFBundleTypeMIMETypes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio/ogg</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Ogg Audio File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>wma</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>wma.icns</string>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>WIndows Media Audio</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>mp3</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>mp3.icns</string>
|
||||||
|
<key>CFBundleTypeMIMETypes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio/mpeg</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>MPEG Audio Layer 3</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>3gp</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>generic.icns</string>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>3GPP File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>m4a</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>mpeg4.icns</string>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>MPEG-4 Audio File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>mpc</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>generic.icns</string>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Musepack Audio File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>flac</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>generic.icns</string>
|
||||||
|
<key>CFBundleTypeMIMETypes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio/flac</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>FLAC Audio File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
||||||
15
dist/macos/Info.plist.in
vendored
15
dist/macos/Info.plist.in
vendored
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>strawberry.icns</string>
|
<string>strawberry.icns</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>org.strawberrymusicplayer.strawberry</string>
|
<string>@MACOS_BUNDLE_ID@</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleLongVersionString</key>
|
<key>CFBundleLongVersionString</key>
|
||||||
@@ -34,17 +34,24 @@
|
|||||||
<string>public.app-category.music</string>
|
<string>public.app-category.music</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>@LSMinimumSystemVersion@</string>
|
<string>@LSMinimumSystemVersion@</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
<!-- Default to manual update checks unless the user explicitly enables automatic checking. -->
|
||||||
|
<key>SUEnableAutomaticChecks</key>
|
||||||
|
<false/>
|
||||||
|
<key>SUAutomaticallyUpdate</key>
|
||||||
|
<false/>
|
||||||
<key>SUFeedURL</key>
|
<key>SUFeedURL</key>
|
||||||
<string>https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@</string>
|
<string>@SPARKLE_FEED_URL@</string>
|
||||||
<key>SUPublicEDKey</key>
|
<key>SUPublicEDKey</key>
|
||||||
<string>/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=</string>
|
<string>@SPARKLE_PUBLIC_ED25519_KEY@</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Viewer</string>
|
<string>Viewer</string>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>org.strawberrymusicplayer.strawberry</string>
|
<string>@MACOS_BUNDLE_ID@</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>tidal</string>
|
<string>tidal</string>
|
||||||
|
|||||||
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
|
||||||
|
|
||||||
20
dist/macos/entitlements.mas.plist
vendored
Normal file
20
dist/macos/entitlements.mas.plist
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!-- Enable the App Sandbox (required for Mac App Store). -->
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- Strawberry is a client app that needs outbound network access (streaming/scrobbling/etc). -->
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- Allow access to user-selected music folders/files (via NSOpenPanel security-scoped bookmarks). -->
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- If iPod classic / other device access is rejected, we'll adjust entitlements after App Review feedback. -->
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
||||||
73
dist/macos/macdeploycheck.sh
vendored
Normal file
73
dist/macos/macdeploycheck.sh
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# macdeploycheck: sanity check a deployed macOS .app bundle for accidental runtime deps
|
||||||
|
# on Homebrew/MacPorts paths (which break distribution / App Store / notarization).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# macdeploycheck /path/to/App.app
|
||||||
|
|
||||||
|
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/MacPorts) runtime deps..."
|
||||||
|
while IFS= read -r f; do
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
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
|
|
||||||
80
dist/macos/privacy_policy.html
vendored
Normal file
80
dist/macos/privacy_policy.html
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Strawberry Music Player — Privacy Policy</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light dark; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 24px; line-height: 1.45; }
|
||||||
|
main { max-width: 900px; margin: 0 auto; }
|
||||||
|
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||||
|
h1,h2 { line-height: 1.15; }
|
||||||
|
.muted { opacity: 0.75; }
|
||||||
|
ul { padding-left: 20px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Privacy Policy</h1>
|
||||||
|
<p class="muted">Last updated: 2026-01-22</p>
|
||||||
|
|
||||||
|
<h2>Summary</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>No analytics / tracking</strong>: This app does not include advertising SDKs, analytics SDKs, or tracking pixels.</li>
|
||||||
|
<li><strong>No data selling</strong>: We do not sell personal data.</li>
|
||||||
|
<li><strong>Optional online features</strong>: If you enable online features (lyrics lookup, cover art search, scrobbling, streaming services, radio, update checks), the app will contact third-party services and send the minimum data needed to provide the feature.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>What data is stored on your device</h2>
|
||||||
|
<p>Strawberry stores data locally on your device, such as:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Library database and playlists (file paths, track metadata, play counts, ratings).</li>
|
||||||
|
<li>App settings and preferences.</li>
|
||||||
|
<li>Optional service credentials/tokens you configure (for example scrobbling or streaming accounts), stored locally.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>What data is sent over the network (and when)</h2>
|
||||||
|
<p>Strawberry does not “phone home” just to run, but it will make network requests when you use or enable specific features. When the app contacts a third-party service, that service will receive standard network information such as your IP address, user-agent, and the request data described below.</p>
|
||||||
|
|
||||||
|
<h3>Album cover art search (optional)</h3>
|
||||||
|
<p>If you use album cover search (or enable “search automatically”), the app may send artist/album/track metadata to configured cover providers to find images.</p>
|
||||||
|
|
||||||
|
<h3>Lyrics lookup (optional)</h3>
|
||||||
|
<p>If you search for lyrics (or enable “search automatically”), the app may send artist/title/album/duration to configured lyrics providers to retrieve lyrics.</p>
|
||||||
|
|
||||||
|
<h3>Scrobbling (optional)</h3>
|
||||||
|
<p>If you enable scrobbling (for example Last.fm or ListenBrainz) the app sends “now playing” and/or listen history data to the configured scrobbling service, including track/artist/album metadata and timestamps. You can disable scrobbling at any time in Settings.</p>
|
||||||
|
|
||||||
|
<h3>Streaming services (optional)</h3>
|
||||||
|
<p>If you enable and sign into a streaming service (for example Tidal, Spotify, Qobuz, Subsonic-compatible servers), the app will communicate with that service to authenticate, browse, and play music. Requests may include account identifiers/tokens and media metadata required by the service.</p>
|
||||||
|
|
||||||
|
<h3>Internet radio (optional)</h3>
|
||||||
|
<p>If you use internet radio features, the app will contact the selected station/provider to retrieve station lists and stream audio.</p>
|
||||||
|
|
||||||
|
<h3>Discord Rich Presence (optional)</h3>
|
||||||
|
<p>If you enable Discord Rich Presence, the app shares currently playing track/artist/album information with the locally-running Discord client so it can be displayed on your Discord profile. You can disable this in Settings.</p>
|
||||||
|
|
||||||
|
<h3>Software updates</h3>
|
||||||
|
<p>The Mac App Store version of Strawberry is updated through Apple’s App Store.</p>
|
||||||
|
|
||||||
|
<h3>OAuth / local redirect server (optional)</h3>
|
||||||
|
<p>Some providers use an OAuth login flow that may open your browser and (in some cases) start a temporary local <code>http://localhost</code> redirect listener to complete authentication. This listener is local-only (not exposed on the internet) and only used during authentication. (Mac App Store builds may disable this flow.)</p>
|
||||||
|
|
||||||
|
<h2>Data sharing</h2>
|
||||||
|
<p>We do not share your personal data with third parties except as necessary to provide features you explicitly use or enable (for example, sending track metadata to a lyrics provider when you request lyrics).</p>
|
||||||
|
|
||||||
|
<h2>Your choices</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Disable Discord Rich Presence in Settings.</li>
|
||||||
|
<li>Disable scrobbling services in Settings.</li>
|
||||||
|
<li>Disable automatic cover/lyrics searching in Settings.</li>
|
||||||
|
<li>Avoid signing into streaming services if you don’t want those network requests.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p>If you have questions about this policy, contact: <strong>privacy@dryark.com</strong></p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -31,10 +31,10 @@
|
|||||||
<li>Edit tags on audio files</li>
|
<li>Edit tags on audio files</li>
|
||||||
<li>Automatically retrieve tags from MusicBrainz</li>
|
<li>Automatically retrieve tags from MusicBrainz</li>
|
||||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||||
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</li>
|
<li>Lyrics from multiple sources</li>
|
||||||
<li>Audio analyzer and equalizer</li>
|
<li>Audio analyzer and equalizer</li>
|
||||||
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
||||||
<li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li>
|
<li>Scrobbler with support for Last.fm and ListenBrainz</li>
|
||||||
<li>Streaming support for Subsonic-compatible servers</li>
|
<li>Streaming support for Subsonic-compatible servers</li>
|
||||||
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -51,6 +51,14 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||||
<releases>
|
<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"/>
|
<release version="1.2.9" date="2025-04-08"/>
|
||||||
<release version="1.2.8" date="2025-04-05"/>
|
<release version="1.2.8" date="2025-04-05"/>
|
||||||
<release version="1.2.7" date="2025-01-31"/>
|
<release version="1.2.7" date="2025-01-31"/>
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ TryExec=strawberry
|
|||||||
Icon=strawberry
|
Icon=strawberry
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=AudioVideo;Player;Qt;Audio;
|
Categories=AudioVideo;Player;Qt;Audio;
|
||||||
Keywords=Audio;Player;
|
Keywords=Audio;Player;Clementine;
|
||||||
StartupNotify=false
|
|
||||||
MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
|
MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
|
||||||
StartupWMClass=strawberry
|
StartupWMClass=strawberry
|
||||||
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
|
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
|
||||||
|
|||||||
6
dist/unix/strawberry.1
vendored
6
dist/unix/strawberry.1
vendored
@@ -29,9 +29,7 @@ Features:
|
|||||||
.br
|
.br
|
||||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||||
.br
|
.br
|
||||||
- Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
- Lyrics from multiple sources
|
||||||
.br
|
|
||||||
- Support for multiple backends
|
|
||||||
.br
|
.br
|
||||||
- Audio analyzer
|
- Audio analyzer
|
||||||
.br
|
.br
|
||||||
@@ -39,7 +37,7 @@ Features:
|
|||||||
.br
|
.br
|
||||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
.br
|
.br
|
||||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
- Scrobbler with support for Last.fm and ListenBrainz
|
||||||
.br
|
.br
|
||||||
- Streaming support for Subsonic-compatible servers
|
- Streaming support for Subsonic-compatible servers
|
||||||
.br
|
.br
|
||||||
|
|||||||
7
dist/unix/strawberry.spec.in
vendored
7
dist/unix/strawberry.spec.in
vendored
@@ -93,16 +93,15 @@ Features:
|
|||||||
- Edit tags on audio files
|
- Edit tags on audio files
|
||||||
- Automatically retrieve tags from MusicBrainz
|
- Automatically retrieve tags from MusicBrainz
|
||||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
- Lyrics from multiple sources
|
||||||
- Support for multiple backends
|
|
||||||
- Audio analyzer
|
- Audio analyzer
|
||||||
- Audio equalizer
|
- Audio equalizer
|
||||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
- Scrobbler with support for Last.fm and ListenBrainz
|
||||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
- Streaming support for Subsonic-compatible servers
|
- Streaming support for Subsonic-compatible servers
|
||||||
- Unofficial streaming support for Tidal and Qobuz
|
- Unofficial streaming support for Tidal and Qobuz
|
||||||
|
|
||||||
%if 0%{?suse_version} && 0%{?suse_version} <= 1600
|
%if 0%{?suse_version} && 0%{?suse_version} < 1600
|
||||||
%debug_package
|
%debug_package
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
|
|||||||
141
dist/windows/strawberry.nsi.in
vendored
141
dist/windows/strawberry.nsi.in
vendored
@@ -21,6 +21,10 @@
|
|||||||
!define arch_x64
|
!define arch_x64
|
||||||
!else if "@ARCH@" == "x86_64-w64-mingw32.shared"
|
!else if "@ARCH@" == "x86_64-w64-mingw32.shared"
|
||||||
!define arch_x64
|
!define arch_x64
|
||||||
|
!else if "@ARCH@" == "arm64"
|
||||||
|
!define arch_arm64
|
||||||
|
!else
|
||||||
|
!error "Missing ARCH"
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!ifdef arch_x86
|
!ifdef arch_x86
|
||||||
@@ -31,6 +35,10 @@
|
|||||||
!define arch "x64"
|
!define arch "x64"
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
|
!ifdef arch_arm64
|
||||||
|
!define arch "arm64"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
|
||||||
!if "@CMAKE_BUILD_TYPE@" == "Release"
|
!if "@CMAKE_BUILD_TYPE@" == "Release"
|
||||||
!define release
|
!define release
|
||||||
@@ -38,6 +46,8 @@
|
|||||||
!define release
|
!define release
|
||||||
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
|
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
|
||||||
!define debug
|
!define debug
|
||||||
|
!else
|
||||||
|
!error "Missing CMAKE_BUILD_TYPE"
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!ifdef release
|
!ifdef release
|
||||||
@@ -70,7 +80,7 @@
|
|||||||
!ifdef arch_x86
|
!ifdef arch_x86
|
||||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug"
|
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug"
|
||||||
!endif
|
!endif
|
||||||
!ifdef arch_x64
|
!ifdef arch_x64 || arch_arm64
|
||||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug"
|
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug"
|
||||||
!endif
|
!endif
|
||||||
!else
|
!else
|
||||||
@@ -80,7 +90,7 @@
|
|||||||
!ifdef arch_x86
|
!ifdef arch_x86
|
||||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player"
|
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player"
|
||||||
!endif
|
!endif
|
||||||
!ifdef arch_x64
|
!ifdef arch_x64 || arch_arm64
|
||||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player"
|
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player"
|
||||||
!endif
|
!endif
|
||||||
!endif
|
!endif
|
||||||
@@ -214,7 +224,7 @@ Function InstallMSVCRuntime
|
|||||||
; ${If} $R0 == ""
|
; ${If} $R0 == ""
|
||||||
SetDetailsView hide
|
SetDetailsView hide
|
||||||
; inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
|
; inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
|
||||||
ExecWait '"$TEMP\${vc_redist_file}" /install /passive'
|
ExecWait '"$TEMP\${vc_redist_file}" /install /passive /norestart'
|
||||||
Delete "$TEMP\${vc_redist_file}"
|
Delete "$TEMP\${vc_redist_file}"
|
||||||
SetDetailsView show
|
SetDetailsView show
|
||||||
; ${EndIf}
|
; ${EndIf}
|
||||||
@@ -324,7 +334,7 @@ Section "Strawberry" Strawberry
|
|||||||
File "libqtsparkle-qt6.dll"
|
File "libqtsparkle-qt6.dll"
|
||||||
File "libsoup-3.0-0.dll"
|
File "libsoup-3.0-0.dll"
|
||||||
File "libspeex-1.dll"
|
File "libspeex-1.dll"
|
||||||
File "libsqlite3.dll"
|
File "libsqlite3-0.dll"
|
||||||
File "libssp-0.dll"
|
File "libssp-0.dll"
|
||||||
File "libstdc++-6.dll"
|
File "libstdc++-6.dll"
|
||||||
File "libtag.dll"
|
File "libtag.dll"
|
||||||
@@ -367,6 +377,10 @@ Section "Strawberry" Strawberry
|
|||||||
File "libcrypto-3-x64.dll"
|
File "libcrypto-3-x64.dll"
|
||||||
File "libssl-3-x64.dll"
|
File "libssl-3-x64.dll"
|
||||||
!endif
|
!endif
|
||||||
|
!ifdef arch_arm64
|
||||||
|
File "libcrypto-3-arm64.dll"
|
||||||
|
File "libssl-3-arm64.dll"
|
||||||
|
!endif
|
||||||
|
|
||||||
File "FLAC.dll"
|
File "FLAC.dll"
|
||||||
File "brotlicommon.dll"
|
File "brotlicommon.dll"
|
||||||
@@ -381,7 +395,6 @@ Section "Strawberry" Strawberry
|
|||||||
File "glib-2.0-0.dll"
|
File "glib-2.0-0.dll"
|
||||||
File "gme.dll"
|
File "gme.dll"
|
||||||
File "gmodule-2.0-0.dll"
|
File "gmodule-2.0-0.dll"
|
||||||
File "gnutls.dll"
|
|
||||||
File "gobject-2.0-0.dll"
|
File "gobject-2.0-0.dll"
|
||||||
File "gstadaptivedemux-1.0-0.dll"
|
File "gstadaptivedemux-1.0-0.dll"
|
||||||
File "gstapp-1.0-0.dll"
|
File "gstapp-1.0-0.dll"
|
||||||
@@ -402,14 +415,11 @@ Section "Strawberry" Strawberry
|
|||||||
File "gsttag-1.0-0.dll"
|
File "gsttag-1.0-0.dll"
|
||||||
File "gsturidownloader-1.0-0.dll"
|
File "gsturidownloader-1.0-0.dll"
|
||||||
File "gstvideo-1.0-0.dll"
|
File "gstvideo-1.0-0.dll"
|
||||||
File "gstwinrt-1.0-0.dll"
|
|
||||||
File "harfbuzz.dll"
|
File "harfbuzz.dll"
|
||||||
File "intl-8.dll"
|
File "intl-8.dll"
|
||||||
File "jpeg62.dll"
|
File "jpeg62.dll"
|
||||||
File "kdsingleapplication-qt6.dll"
|
File "kdsingleapplication-qt6.dll"
|
||||||
File "libbs2b.dll"
|
File "libbs2b.dll"
|
||||||
File "libfaac_dll.dll"
|
|
||||||
File "liblzma.dll"
|
|
||||||
File "libmp3lame.dll"
|
File "libmp3lame.dll"
|
||||||
File "libopenmpt.dll"
|
File "libopenmpt.dll"
|
||||||
File "mpcdec.dll"
|
File "mpcdec.dll"
|
||||||
@@ -427,6 +437,11 @@ Section "Strawberry" Strawberry
|
|||||||
File "vorbisfile.dll"
|
File "vorbisfile.dll"
|
||||||
File "wavpackdll.dll"
|
File "wavpackdll.dll"
|
||||||
|
|
||||||
|
!ifndef arch_arm64
|
||||||
|
File "gnutls.dll"
|
||||||
|
File "libfaac_dll.dll"
|
||||||
|
!endif
|
||||||
|
|
||||||
!ifdef release
|
!ifdef release
|
||||||
File "freetype.dll"
|
File "freetype.dll"
|
||||||
File "libiconv.dll"
|
File "libiconv.dll"
|
||||||
@@ -434,8 +449,10 @@ Section "Strawberry" Strawberry
|
|||||||
File "libspeex.dll"
|
File "libspeex.dll"
|
||||||
File "pcre2-8.dll"
|
File "pcre2-8.dll"
|
||||||
File "pcre2-16.dll"
|
File "pcre2-16.dll"
|
||||||
|
File "zlib1.dll"
|
||||||
|
!ifndef arch_arm64
|
||||||
File "twolame.dll"
|
File "twolame.dll"
|
||||||
File "zlib.dll"
|
!endif
|
||||||
!endif
|
!endif
|
||||||
!ifdef debug
|
!ifdef debug
|
||||||
File "freetyped.dll"
|
File "freetyped.dll"
|
||||||
@@ -444,8 +461,10 @@ Section "Strawberry" Strawberry
|
|||||||
File "libspeexd.dll"
|
File "libspeexd.dll"
|
||||||
File "pcre2-8d.dll"
|
File "pcre2-8d.dll"
|
||||||
File "pcre2-16d.dll"
|
File "pcre2-16d.dll"
|
||||||
|
File "zlibd1.dll"
|
||||||
|
!ifndef arch_arm64
|
||||||
File "twolamed.dll"
|
File "twolamed.dll"
|
||||||
File "zlibd.dll"
|
!endif
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
|
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
|
||||||
@@ -458,11 +477,15 @@ Section "Strawberry" Strawberry
|
|||||||
|
|
||||||
; Common files
|
; Common files
|
||||||
|
|
||||||
File "icudt77.dll"
|
File "icudt78.dll"
|
||||||
|
!ifdef msvc && arch_arm64
|
||||||
|
File "fftw3.dll"
|
||||||
|
!else
|
||||||
File "libfftw3-3.dll"
|
File "libfftw3-3.dll"
|
||||||
|
!endif
|
||||||
!ifdef msvc && debug
|
!ifdef msvc && debug
|
||||||
File "icuin77d.dll"
|
File "icuin78d.dll"
|
||||||
File "icuuc77d.dll"
|
File "icuuc78d.dll"
|
||||||
File "libxml2d.dll"
|
File "libxml2d.dll"
|
||||||
File "Qt6Concurrentd.dll"
|
File "Qt6Concurrentd.dll"
|
||||||
File "Qt6Cored.dll"
|
File "Qt6Cored.dll"
|
||||||
@@ -471,8 +494,8 @@ Section "Strawberry" Strawberry
|
|||||||
File "Qt6Sqld.dll"
|
File "Qt6Sqld.dll"
|
||||||
File "Qt6Widgetsd.dll"
|
File "Qt6Widgetsd.dll"
|
||||||
!else
|
!else
|
||||||
File "icuin77.dll"
|
File "icuin78.dll"
|
||||||
File "icuuc77.dll"
|
File "icuuc78.dll"
|
||||||
File "libxml2.dll"
|
File "libxml2.dll"
|
||||||
File "Qt6Concurrent.dll"
|
File "Qt6Concurrent.dll"
|
||||||
File "Qt6Core.dll"
|
File "Qt6Core.dll"
|
||||||
@@ -482,6 +505,7 @@ Section "Strawberry" Strawberry
|
|||||||
File "Qt6Widgets.dll"
|
File "Qt6Widgets.dll"
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
|
!ifdef msvc && arch_x86
|
||||||
File "avcodec-61.dll"
|
File "avcodec-61.dll"
|
||||||
File "avfilter-10.dll"
|
File "avfilter-10.dll"
|
||||||
File "avformat-61.dll"
|
File "avformat-61.dll"
|
||||||
@@ -489,6 +513,14 @@ Section "Strawberry" Strawberry
|
|||||||
File "postproc-58.dll"
|
File "postproc-58.dll"
|
||||||
File "swresample-5.dll"
|
File "swresample-5.dll"
|
||||||
File "swscale-8.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
|
; Register Strawberry with Default Programs
|
||||||
Var /GLOBAL AppIcon
|
Var /GLOBAL AppIcon
|
||||||
@@ -526,11 +558,13 @@ Section "GIO modules" gio-modules
|
|||||||
SetOutPath "$INSTDIR\gio-modules"
|
SetOutPath "$INSTDIR\gio-modules"
|
||||||
!ifdef mingw
|
!ifdef mingw
|
||||||
File "/oname=libgiognutls.dll" "gio-modules\libgiognutls.dll"
|
File "/oname=libgiognutls.dll" "gio-modules\libgiognutls.dll"
|
||||||
File "/oname=libgioopenssl.dll" "gio-modules\libgioopenssl.dll"
|
|
||||||
!endif
|
!endif
|
||||||
!ifdef msvc
|
!ifdef msvc
|
||||||
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
|
!ifdef arch_arm64
|
||||||
File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll"
|
File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll"
|
||||||
|
!else
|
||||||
|
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
|
||||||
|
!endif
|
||||||
!endif
|
!endif
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
@@ -647,6 +681,7 @@ Section "Gstreamer plugins" gstreamer-plugins
|
|||||||
File "/oname=libgstvolume.dll" "gstreamer-plugins\libgstvolume.dll"
|
File "/oname=libgstvolume.dll" "gstreamer-plugins\libgstvolume.dll"
|
||||||
File "/oname=libgstvorbis.dll" "gstreamer-plugins\libgstvorbis.dll"
|
File "/oname=libgstvorbis.dll" "gstreamer-plugins\libgstvorbis.dll"
|
||||||
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
|
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
|
||||||
|
File "/oname=libgstwasapi2.dll" "gstreamer-plugins\libgstwasapi2.dll"
|
||||||
File "/oname=libgstwaveform.dll" "gstreamer-plugins\libgstwaveform.dll"
|
File "/oname=libgstwaveform.dll" "gstreamer-plugins\libgstwaveform.dll"
|
||||||
File "/oname=libgstwavenc.dll" "gstreamer-plugins\libgstwavenc.dll"
|
File "/oname=libgstwavenc.dll" "gstreamer-plugins\libgstwavenc.dll"
|
||||||
File "/oname=libgstwavpack.dll" "gstreamer-plugins\libgstwavpack.dll"
|
File "/oname=libgstwavpack.dll" "gstreamer-plugins\libgstwavpack.dll"
|
||||||
@@ -674,7 +709,6 @@ Section "Gstreamer plugins" gstreamer-plugins
|
|||||||
File "/oname=gstdirectsound.dll" "gstreamer-plugins\gstdirectsound.dll"
|
File "/oname=gstdirectsound.dll" "gstreamer-plugins\gstdirectsound.dll"
|
||||||
File "/oname=gstdsd.dll" "gstreamer-plugins\gstdsd.dll"
|
File "/oname=gstdsd.dll" "gstreamer-plugins\gstdsd.dll"
|
||||||
File "/oname=gstequalizer.dll" "gstreamer-plugins\gstequalizer.dll"
|
File "/oname=gstequalizer.dll" "gstreamer-plugins\gstequalizer.dll"
|
||||||
File "/oname=gstfaac.dll" "gstreamer-plugins\gstfaac.dll"
|
|
||||||
File "/oname=gstfaad.dll" "gstreamer-plugins\gstfaad.dll"
|
File "/oname=gstfaad.dll" "gstreamer-plugins\gstfaad.dll"
|
||||||
File "/oname=gstfdkaac.dll" "gstreamer-plugins\gstfdkaac.dll"
|
File "/oname=gstfdkaac.dll" "gstreamer-plugins\gstfdkaac.dll"
|
||||||
File "/oname=gstflac.dll" "gstreamer-plugins\gstflac.dll"
|
File "/oname=gstflac.dll" "gstreamer-plugins\gstflac.dll"
|
||||||
@@ -707,7 +741,6 @@ Section "Gstreamer plugins" gstreamer-plugins
|
|||||||
File "/oname=gstspeex.dll" "gstreamer-plugins\gstspeex.dll"
|
File "/oname=gstspeex.dll" "gstreamer-plugins\gstspeex.dll"
|
||||||
File "/oname=gsttaglib.dll" "gstreamer-plugins\gsttaglib.dll"
|
File "/oname=gsttaglib.dll" "gstreamer-plugins\gsttaglib.dll"
|
||||||
File "/oname=gsttcp.dll" "gstreamer-plugins\gsttcp.dll"
|
File "/oname=gsttcp.dll" "gstreamer-plugins\gsttcp.dll"
|
||||||
File "/oname=gsttwolame.dll" "gstreamer-plugins\gsttwolame.dll"
|
|
||||||
File "/oname=gsttypefindfunctions.dll" "gstreamer-plugins\gsttypefindfunctions.dll"
|
File "/oname=gsttypefindfunctions.dll" "gstreamer-plugins\gsttypefindfunctions.dll"
|
||||||
File "/oname=gstudp.dll" "gstreamer-plugins\gstudp.dll"
|
File "/oname=gstudp.dll" "gstreamer-plugins\gstudp.dll"
|
||||||
File "/oname=gstvolume.dll" "gstreamer-plugins\gstvolume.dll"
|
File "/oname=gstvolume.dll" "gstreamer-plugins\gstvolume.dll"
|
||||||
@@ -719,8 +752,12 @@ Section "Gstreamer plugins" gstreamer-plugins
|
|||||||
File "/oname=gstwavpack.dll" "gstreamer-plugins\gstwavpack.dll"
|
File "/oname=gstwavpack.dll" "gstreamer-plugins\gstwavpack.dll"
|
||||||
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
|
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
|
||||||
File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll"
|
File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll"
|
||||||
|
!ifndef arch_arm64
|
||||||
|
File "/oname=gstfaac.dll" "gstreamer-plugins\gstfaac.dll"
|
||||||
|
File "/oname=gsttwolame.dll" "gstreamer-plugins\gsttwolame.dll"
|
||||||
|
!endif
|
||||||
!ifdef arch_x64
|
!ifdef arch_x64
|
||||||
;File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
|
File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
|
||||||
!endif
|
!endif
|
||||||
!endif ; MSVC
|
!endif ; MSVC
|
||||||
|
|
||||||
@@ -849,7 +886,7 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\libqtsparkle-qt6.dll"
|
Delete "$INSTDIR\libqtsparkle-qt6.dll"
|
||||||
Delete "$INSTDIR\libsoup-3.0-0.dll"
|
Delete "$INSTDIR\libsoup-3.0-0.dll"
|
||||||
Delete "$INSTDIR\libspeex-1.dll"
|
Delete "$INSTDIR\libspeex-1.dll"
|
||||||
Delete "$INSTDIR\libsqlite3.dll"
|
Delete "$INSTDIR\libsqlite3-0.dll"
|
||||||
Delete "$INSTDIR\libssp-0.dll"
|
Delete "$INSTDIR\libssp-0.dll"
|
||||||
Delete "$INSTDIR\libstdc++-6.dll"
|
Delete "$INSTDIR\libstdc++-6.dll"
|
||||||
Delete "$INSTDIR\libtag.dll"
|
Delete "$INSTDIR\libtag.dll"
|
||||||
@@ -892,6 +929,10 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\libcrypto-3-x64.dll"
|
Delete "$INSTDIR\libcrypto-3-x64.dll"
|
||||||
Delete "$INSTDIR\libssl-3-x64.dll"
|
Delete "$INSTDIR\libssl-3-x64.dll"
|
||||||
!endif
|
!endif
|
||||||
|
!ifdef arch_arm64
|
||||||
|
Delete "$INSTDIR\libcrypto-3-arm64.dll"
|
||||||
|
Delete "$INSTDIR\libssl-3-arm64.dll"
|
||||||
|
!endif
|
||||||
|
|
||||||
Delete "$INSTDIR\FLAC.dll"
|
Delete "$INSTDIR\FLAC.dll"
|
||||||
Delete "$INSTDIR\brotlicommon.dll"
|
Delete "$INSTDIR\brotlicommon.dll"
|
||||||
@@ -906,7 +947,6 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\glib-2.0-0.dll"
|
Delete "$INSTDIR\glib-2.0-0.dll"
|
||||||
Delete "$INSTDIR\gme.dll"
|
Delete "$INSTDIR\gme.dll"
|
||||||
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
||||||
Delete "$INSTDIR\gnutls.dll"
|
|
||||||
Delete "$INSTDIR\gobject-2.0-0.dll"
|
Delete "$INSTDIR\gobject-2.0-0.dll"
|
||||||
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
|
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
|
||||||
Delete "$INSTDIR\gstapp-1.0-0.dll"
|
Delete "$INSTDIR\gstapp-1.0-0.dll"
|
||||||
@@ -927,14 +967,11 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\gsttag-1.0-0.dll"
|
Delete "$INSTDIR\gsttag-1.0-0.dll"
|
||||||
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
|
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
|
||||||
Delete "$INSTDIR\gstvideo-1.0-0.dll"
|
Delete "$INSTDIR\gstvideo-1.0-0.dll"
|
||||||
Delete "$INSTDIR\gstwinrt-1.0-0.dll"
|
|
||||||
Delete "$INSTDIR\harfbuzz.dll"
|
Delete "$INSTDIR\harfbuzz.dll"
|
||||||
Delete "$INSTDIR\intl-8.dll"
|
Delete "$INSTDIR\intl-8.dll"
|
||||||
Delete "$INSTDIR\jpeg62.dll"
|
Delete "$INSTDIR\jpeg62.dll"
|
||||||
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
|
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
|
||||||
Delete "$INSTDIR\libbs2b.dll"
|
Delete "$INSTDIR\libbs2b.dll"
|
||||||
Delete "$INSTDIR\libfaac_dll.dll"
|
|
||||||
Delete "$INSTDIR\liblzma.dll"
|
|
||||||
Delete "$INSTDIR\libmp3lame.dll"
|
Delete "$INSTDIR\libmp3lame.dll"
|
||||||
Delete "$INSTDIR\libopenmpt.dll"
|
Delete "$INSTDIR\libopenmpt.dll"
|
||||||
Delete "$INSTDIR\mpcdec.dll"
|
Delete "$INSTDIR\mpcdec.dll"
|
||||||
@@ -952,6 +989,11 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\vorbisfile.dll"
|
Delete "$INSTDIR\vorbisfile.dll"
|
||||||
Delete "$INSTDIR\wavpackdll.dll"
|
Delete "$INSTDIR\wavpackdll.dll"
|
||||||
|
|
||||||
|
!ifndef arch_arm64
|
||||||
|
Delete "$INSTDIR\gnutls.dll"
|
||||||
|
Delete "$INSTDIR\libfaac_dll.dll"
|
||||||
|
!endif
|
||||||
|
|
||||||
!ifdef release
|
!ifdef release
|
||||||
Delete "$INSTDIR\freetype.dll"
|
Delete "$INSTDIR\freetype.dll"
|
||||||
Delete "$INSTDIR\libiconv.dll"
|
Delete "$INSTDIR\libiconv.dll"
|
||||||
@@ -959,8 +1001,10 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\libspeex.dll"
|
Delete "$INSTDIR\libspeex.dll"
|
||||||
Delete "$INSTDIR\pcre2-8.dll"
|
Delete "$INSTDIR\pcre2-8.dll"
|
||||||
Delete "$INSTDIR\pcre2-16.dll"
|
Delete "$INSTDIR\pcre2-16.dll"
|
||||||
|
Delete "$INSTDIR\zlib1.dll"
|
||||||
|
!ifndef arch_arm64
|
||||||
Delete "$INSTDIR\twolame.dll"
|
Delete "$INSTDIR\twolame.dll"
|
||||||
Delete "$INSTDIR\zlib.dll"
|
!endif
|
||||||
!endif
|
!endif
|
||||||
!ifdef debug
|
!ifdef debug
|
||||||
Delete "$INSTDIR\freetyped.dll"
|
Delete "$INSTDIR\freetyped.dll"
|
||||||
@@ -969,8 +1013,10 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\libspeexd.dll"
|
Delete "$INSTDIR\libspeexd.dll"
|
||||||
Delete "$INSTDIR\pcre2-8d.dll"
|
Delete "$INSTDIR\pcre2-8d.dll"
|
||||||
Delete "$INSTDIR\pcre2-16d.dll"
|
Delete "$INSTDIR\pcre2-16d.dll"
|
||||||
|
Delete "$INSTDIR\zlibd1.dll"
|
||||||
|
!ifndef arch_arm64
|
||||||
Delete "$INSTDIR\twolamed.dll"
|
Delete "$INSTDIR\twolamed.dll"
|
||||||
Delete "$INSTDIR\zlibd.dll"
|
!endif
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!ifdef arch_x86
|
!ifdef arch_x86
|
||||||
@@ -982,11 +1028,15 @@ Section "Uninstall"
|
|||||||
|
|
||||||
; Common files
|
; Common files
|
||||||
|
|
||||||
Delete "$INSTDIR\icudt77.dll"
|
Delete "$INSTDIR\icudt78.dll"
|
||||||
|
!ifdef msvc && arch_arm64
|
||||||
|
Delete "$INSTDIR\fftw3.dll"
|
||||||
|
!else
|
||||||
Delete "$INSTDIR\libfftw3-3.dll"
|
Delete "$INSTDIR\libfftw3-3.dll"
|
||||||
|
!endif
|
||||||
!ifdef msvc && debug
|
!ifdef msvc && debug
|
||||||
Delete "$INSTDIR\icuin77d.dll"
|
Delete "$INSTDIR\icuin78d.dll"
|
||||||
Delete "$INSTDIR\icuuc77d.dll"
|
Delete "$INSTDIR\icuuc78d.dll"
|
||||||
Delete "$INSTDIR\libxml2d.dll"
|
Delete "$INSTDIR\libxml2d.dll"
|
||||||
Delete "$INSTDIR\Qt6Concurrentd.dll"
|
Delete "$INSTDIR\Qt6Concurrentd.dll"
|
||||||
Delete "$INSTDIR\Qt6Cored.dll"
|
Delete "$INSTDIR\Qt6Cored.dll"
|
||||||
@@ -995,8 +1045,8 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\Qt6Sqld.dll"
|
Delete "$INSTDIR\Qt6Sqld.dll"
|
||||||
Delete "$INSTDIR\Qt6Widgetsd.dll"
|
Delete "$INSTDIR\Qt6Widgetsd.dll"
|
||||||
!else
|
!else
|
||||||
Delete "$INSTDIR\icuin77.dll"
|
Delete "$INSTDIR\icuin78.dll"
|
||||||
Delete "$INSTDIR\icuuc77.dll"
|
Delete "$INSTDIR\icuuc78.dll"
|
||||||
Delete "$INSTDIR\libxml2.dll"
|
Delete "$INSTDIR\libxml2.dll"
|
||||||
Delete "$INSTDIR\Qt6Concurrent.dll"
|
Delete "$INSTDIR\Qt6Concurrent.dll"
|
||||||
Delete "$INSTDIR\Qt6Core.dll"
|
Delete "$INSTDIR\Qt6Core.dll"
|
||||||
@@ -1006,6 +1056,7 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\Qt6Widgets.dll"
|
Delete "$INSTDIR\Qt6Widgets.dll"
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
|
!ifdef msvc && arch_x86
|
||||||
Delete "$INSTDIR\avcodec-61.dll"
|
Delete "$INSTDIR\avcodec-61.dll"
|
||||||
Delete "$INSTDIR\avfilter-10.dll"
|
Delete "$INSTDIR\avfilter-10.dll"
|
||||||
Delete "$INSTDIR\avformat-61.dll"
|
Delete "$INSTDIR\avformat-61.dll"
|
||||||
@@ -1013,14 +1064,24 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\postproc-58.dll"
|
Delete "$INSTDIR\postproc-58.dll"
|
||||||
Delete "$INSTDIR\swresample-5.dll"
|
Delete "$INSTDIR\swresample-5.dll"
|
||||||
Delete "$INSTDIR\swscale-8.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
|
!ifdef mingw
|
||||||
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
|
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
|
||||||
Delete "$INSTDIR\gio-modules\libgioopenssl.dll"
|
|
||||||
!endif
|
!endif
|
||||||
!ifdef msvc
|
!ifdef msvc
|
||||||
Delete "$INSTDIR\gio-modules\giognutls.dll"
|
!ifdef arch_arm64
|
||||||
Delete "$INSTDIR\gio-modules\gioopenssl.dll"
|
Delete "$INSTDIR\gio-modules\gioopenssl.dll"
|
||||||
|
!else
|
||||||
|
Delete "$INSTDIR\gio-modules\giognutls.dll"
|
||||||
|
!endif
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!ifdef msvc && debug
|
!ifdef msvc && debug
|
||||||
@@ -1104,6 +1165,7 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\gstreamer-plugins\libgstvolume.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstvolume.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\libgstvorbis.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstvorbis.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
|
||||||
|
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi2.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\libgstwaveform.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstwaveform.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavenc.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstwavenc.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavpack.dll"
|
Delete "$INSTDIR\gstreamer-plugins\libgstwavpack.dll"
|
||||||
@@ -1133,7 +1195,6 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\gstreamer-plugins\gstdirectsound.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstdirectsound.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstdsd.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstdsd.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstequalizer.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstequalizer.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
|
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstfaad.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstfaad.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstfdkaac.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstfdkaac.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstflac.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstflac.dll"
|
||||||
@@ -1166,7 +1227,6 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\gstreamer-plugins\gstspeex.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstspeex.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gsttaglib.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gsttaglib.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gsttcp.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gsttcp.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
|
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gsttypefindfunctions.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gsttypefindfunctions.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstudp.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstudp.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstvolume.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstvolume.dll"
|
||||||
@@ -1178,9 +1238,14 @@ Section "Uninstall"
|
|||||||
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
|
||||||
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
|
||||||
!ifdef arch_x64
|
!ifndef arch_arm64
|
||||||
;Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
|
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
|
||||||
|
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
|
||||||
!endif
|
!endif
|
||||||
|
!ifdef arch_x64
|
||||||
|
Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
|
||||||
|
!endif
|
||||||
|
|
||||||
!endif ; msvc
|
!endif ; msvc
|
||||||
|
|
||||||
Delete "$INSTDIR\Uninstall.exe"
|
Delete "$INSTDIR\Uninstall.exe"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef ANALYZERBASE_H
|
#ifndef ANALYZERBASE_H
|
||||||
#define ANALYZERBASE_H
|
#define ANALYZERBASE_H
|
||||||
@@ -90,4 +90,3 @@ class AnalyzerBase : public QWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // ANALYZERBASE_H
|
#endif // ANALYZERBASE_H
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ constexpr int kLowFramerate = 20;
|
|||||||
constexpr int kMediumFramerate = 25;
|
constexpr int kMediumFramerate = 25;
|
||||||
constexpr int kHighFramerate = 30;
|
constexpr int kHighFramerate = 30;
|
||||||
constexpr int kSuperHighFramerate = 60;
|
constexpr int kSuperHighFramerate = 60;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AnalyzerContainer::AnalyzerContainer(QWidget *parent)
|
AnalyzerContainer::AnalyzerContainer(QWidget *parent)
|
||||||
: QWidget(parent),
|
: QWidget(parent),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef ANALYZERCONTAINER_H
|
#ifndef ANALYZERCONTAINER_H
|
||||||
#define ANALYZERCONTAINER_H
|
#define ANALYZERCONTAINER_H
|
||||||
@@ -101,8 +101,7 @@ void AnalyzerContainer::AddAnalyzerType() {
|
|||||||
group_->addAction(action);
|
group_->addAction(action);
|
||||||
action->setCheckable(true);
|
action->setCheckable(true);
|
||||||
actions_ << action;
|
actions_ << action;
|
||||||
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); } );
|
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // ANALYZERCONTAINER_H
|
#endif // ANALYZERCONTAINER_H
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "blockanalyzer.h"
|
#include "blockanalyzer.h"
|
||||||
|
|
||||||
@@ -61,11 +61,12 @@ BlockAnalyzer::BlockAnalyzer(QWidget *parent)
|
|||||||
fade_intensity_(1 << 8, 32),
|
fade_intensity_(1 << 8, 32),
|
||||||
step_(0) {
|
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);
|
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
|
||||||
|
|
||||||
// mxcl says null pixmaps cause crashes, so let's play it safe
|
// mxcl says null pixmaps cause crashes, so let's play it safe
|
||||||
std::fill(fade_bars_.begin(), fade_bars_.end(), QPixmap(1, 1));
|
std::fill(fade_bars_.begin(), fade_bars_.end(), QPixmap(1, 1));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
|
void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
|
||||||
@@ -237,7 +238,7 @@ static inline void adjustToLimits(const int b, int &f, int &amount) {
|
|||||||
* Clever contrast function
|
* Clever contrast function
|
||||||
*
|
*
|
||||||
* It will try to adjust the foreground color such that it contrasts well with
|
* 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
|
* It won't modify the hue of fg unless absolutely necessary
|
||||||
* @return the adjusted form of fg
|
* @return the adjusted form of fg
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef BLOCKANALYZER_H
|
#ifndef BLOCKANALYZER_H
|
||||||
#define BLOCKANALYZER_H
|
#define BLOCKANALYZER_H
|
||||||
@@ -41,14 +41,14 @@ class BlockAnalyzer : public AnalyzerBase {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Q_INVOKABLE explicit BlockAnalyzer(QWidget*);
|
Q_INVOKABLE explicit BlockAnalyzer(QWidget *parent);
|
||||||
|
|
||||||
static const char *kName;
|
static const char *kName;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void transform(Scope&) override;
|
void transform(Scope &s) override;
|
||||||
void analyze(QPainter &p, const Scope &s, const bool new_frame) 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);
|
virtual void paletteChange(const QPalette &_palette);
|
||||||
void framerateChanged() override;
|
void framerateChanged() override;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "boomanalyzer.h"
|
#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]);
|
bar_height_[i] = std::max(0.0, bar_height_[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
peak_handling:
|
peak_handling:
|
||||||
|
|
||||||
if (peak_height_[i] > 0.0) {
|
if (peak_height_[i] > 0.0) {
|
||||||
peak_height_[i] -= peak_speed_[i];
|
peak_height_[i] -= peak_speed_[i];
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef BOOMANALYZER_H
|
#ifndef BOOMANALYZER_H
|
||||||
#define BOOMANALYZER_H
|
#define BOOMANALYZER_H
|
||||||
@@ -40,7 +40,7 @@ class BoomAnalyzer : public AnalyzerBase {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Q_INVOKABLE explicit BoomAnalyzer(QWidget*);
|
Q_INVOKABLE explicit BoomAnalyzer(QWidget *parent);
|
||||||
|
|
||||||
static const char *kName;
|
static const char *kName;
|
||||||
|
|
||||||
@@ -70,7 +70,6 @@ class BoomAnalyzer : public AnalyzerBase {
|
|||||||
|
|
||||||
QPixmap barPixmap_;
|
QPixmap barPixmap_;
|
||||||
QPixmap canvas_;
|
QPixmap canvas_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BOOMANALYZER_H
|
#endif // BOOMANALYZER_H
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "fht.h"
|
#include "fht.h"
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef FHT_H
|
#ifndef FHT_H
|
||||||
#define FHT_H
|
#define FHT_H
|
||||||
@@ -55,20 +55,20 @@ class FHT {
|
|||||||
/**
|
/**
|
||||||
* Recursive in-place Hartley transform. For internal use only!
|
* Recursive in-place Hartley transform. For internal use only!
|
||||||
*/
|
*/
|
||||||
void _transform(float*, int, int);
|
void _transform(float *p, int n, int k);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* Prepare transform for data sets with @f$2^n@f$ numbers, whereby @f$n@f$
|
* 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.
|
* should be at least 3. Values of more than 3 need a trigonometry table.
|
||||||
* @see makeCasTable()
|
* @see makeCasTable()
|
||||||
*/
|
*/
|
||||||
explicit FHT(uint);
|
explicit FHT(uint);
|
||||||
|
|
||||||
~FHT();
|
~FHT();
|
||||||
int sizeExp() const;
|
int sizeExp() const;
|
||||||
int size() const;
|
int size() const;
|
||||||
void scale(float*, float) const;
|
void scale(float *p, float d) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exponentially Weighted Moving Average (EWMA) filter.
|
* Exponentially Weighted Moving Average (EWMA) filter.
|
||||||
@@ -90,12 +90,12 @@ class FHT {
|
|||||||
/**
|
/**
|
||||||
* Semi-logarithmic audio spectrum.
|
* Semi-logarithmic audio spectrum.
|
||||||
*/
|
*/
|
||||||
void semiLogSpectrum(float*);
|
void semiLogSpectrum(float *p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fourier spectrum.
|
* Fourier spectrum.
|
||||||
*/
|
*/
|
||||||
void spectrum(float*);
|
void spectrum(float *p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates a mathematically correct FFT power spectrum.
|
* Calculates a mathematically correct FFT power spectrum.
|
||||||
@@ -103,7 +103,7 @@ class FHT {
|
|||||||
* and factor the 0.5 in the final scaling factor.
|
* and factor the 0.5 in the final scaling factor.
|
||||||
* @see FHT::power2()
|
* @see FHT::power2()
|
||||||
*/
|
*/
|
||||||
void power(float*);
|
void power(float *p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates an FFT power spectrum with doubled values as a
|
* 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.
|
* of @f$2^n@f$ input values. This is the fastest transform.
|
||||||
* @see FHT::power()
|
* @see FHT::power()
|
||||||
*/
|
*/
|
||||||
void power2(float*);
|
void power2(float *p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discrete Hartley transform of data sets with 8 values.
|
* 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
|
#endif // FHT_H
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "rainbowanalyzer.h"
|
#include "rainbowanalyzer.h"
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef RAINBOWANALYZER_H
|
#ifndef RAINBOWANALYZER_H
|
||||||
#define RAINBOWANALYZER_H
|
#define RAINBOWANALYZER_H
|
||||||
@@ -85,7 +85,7 @@ class RainbowAnalyzer : public AnalyzerBase {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
// "constants" that get initialized in the constructor
|
// "constants" that get initialized in the constructor
|
||||||
float band_scale_[kRainbowBands] {};
|
float band_scale_[kRainbowBands]{};
|
||||||
QPen colors_[kRainbowBands];
|
QPen colors_[kRainbowBands];
|
||||||
|
|
||||||
// Rainbow Nyancat & Dash
|
// Rainbow Nyancat & Dash
|
||||||
@@ -96,7 +96,7 @@ class RainbowAnalyzer : public AnalyzerBase {
|
|||||||
int frame_;
|
int frame_;
|
||||||
|
|
||||||
// The y positions of each point on the rainbow.
|
// 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,
|
// A cache of the last frame's rainbow,
|
||||||
// so it can be used in the next frame.
|
// 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
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef SONOGRAMANALYZER_H
|
#ifndef SONOGRAMANALYZER_H
|
||||||
#define SONOGRAMANALYZER_H
|
#define SONOGRAMANALYZER_H
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#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]);
|
bar_height_[i] = std::max(0.0, bar_height_[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
peak_handling:
|
peak_handling:
|
||||||
if (peak_height_[i] > 0.0) {
|
if (peak_height_[i] > 0.0) {
|
||||||
peak_height_[i] -= peak_speed_[i];
|
peak_height_[i] -= peak_speed_[i];
|
||||||
peak_speed_[i] *= F_peakSpeed_; // 1.12
|
peak_speed_[i] *= F_peakSpeed_; // 1.12
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef TURBINEANALYZER_H
|
#ifndef TURBINEANALYZER_H
|
||||||
#define TURBINEANALYZER_H
|
#define TURBINEANALYZER_H
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -537,10 +537,24 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
|||||||
|
|
||||||
ScopedTransaction transaction(&db);
|
ScopedTransaction transaction(&db);
|
||||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||||
if (subdir.mtime == 0) {
|
// See if this subdirectory already exists in the database
|
||||||
// Delete the subdirectory
|
bool exists = false;
|
||||||
|
{
|
||||||
SqlQuery q(db);
|
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":id"_s, subdir.directory_id);
|
||||||
q.BindValue(u":path"_s, subdir.path);
|
q.BindValue(u":path"_s, subdir.path);
|
||||||
if (!q.Exec()) {
|
if (!q.Exec()) {
|
||||||
@@ -549,42 +563,36 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// See if this subdirectory already exists in the database
|
SqlQuery q(db);
|
||||||
bool exists = false;
|
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);
|
||||||
SqlQuery q(db);
|
q.BindValue(u":path"_s, subdir.path);
|
||||||
q.prepare(QStringLiteral("SELECT ROWID FROM %1 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);
|
if (!q.Exec()) {
|
||||||
q.BindValue(u":path"_s, subdir.path);
|
db_->ReportErrors(q);
|
||||||
if (!q.Exec()) {
|
return;
|
||||||
db_->ReportErrors(q);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
exists = q.next();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (exists) {
|
transaction.Commit();
|
||||||
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);
|
void CollectionBackend::DeleteSubdirs(const CollectionSubdirectoryList &subdirs) {
|
||||||
q.BindValue(u":path"_s, subdir.path);
|
|
||||||
if (!q.Exec()) {
|
QMutexLocker l(db_->Mutex());
|
||||||
db_->ReportErrors(q);
|
QSqlDatabase db(db_->Connect());
|
||||||
return;
|
|
||||||
}
|
ScopedTransaction transaction(&db);
|
||||||
}
|
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||||
else {
|
SqlQuery q(db);
|
||||||
SqlQuery q(db);
|
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||||
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":id"_s, subdir.directory_id);
|
q.BindValue(u":path"_s, subdir.path);
|
||||||
q.BindValue(u":path"_s, subdir.path);
|
if (!q.Exec()) {
|
||||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
db_->ReportErrors(q);
|
||||||
if (!q.Exec()) {
|
return;
|
||||||
db_->ReportErrors(q);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,6 +631,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
|||||||
QMutexLocker l(db_->Mutex());
|
QMutexLocker l(db_->Mutex());
|
||||||
QSqlDatabase db(db_->Connect());
|
QSqlDatabase db(db_->Connect());
|
||||||
|
|
||||||
|
CollectionTask task(task_manager_, tr("Updating %1 database.").arg(Song::TextForSource(source_)));
|
||||||
ScopedTransaction transaction(&db);
|
ScopedTransaction transaction(&db);
|
||||||
|
|
||||||
SongList added_songs;
|
SongList added_songs;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-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
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -140,7 +140,6 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
Q_INVOKABLE explicit CollectionBackend(QObject *parent = nullptr);
|
Q_INVOKABLE explicit CollectionBackend(QObject *parent = nullptr);
|
||||||
|
|
||||||
~CollectionBackend();
|
~CollectionBackend();
|
||||||
@@ -253,6 +252,7 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||||||
void DeleteSongsByUrls(const QList<QUrl> &url);
|
void DeleteSongsByUrls(const QList<QUrl> &url);
|
||||||
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
|
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
|
||||||
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
|
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||||
|
void DeleteSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||||
void CompilationsNeedUpdating();
|
void CompilationsNeedUpdating();
|
||||||
void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded);
|
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);
|
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual);
|
||||||
@@ -331,4 +331,3 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // COLLECTIONBACKEND_H
|
#endif // COLLECTIONBACKEND_H
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
|
|
||||||
class CollectionFilterOptions {
|
class CollectionFilterOptions {
|
||||||
public:
|
public:
|
||||||
|
|
||||||
explicit CollectionFilterOptions();
|
explicit CollectionFilterOptions();
|
||||||
|
|
||||||
// Filter mode:
|
// Filter mode:
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include <QUrl>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
@@ -295,19 +296,21 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
|
|||||||
if (version == 1) {
|
if (version == 1) {
|
||||||
QStringList saved = s.childKeys();
|
QStringList saved = s.childKeys();
|
||||||
for (int i = 0; i < saved.size(); ++i) {
|
for (int i = 0; i < saved.size(); ++i) {
|
||||||
if (saved.at(i) == "version"_L1) continue;
|
const QString &name = saved.at(i);
|
||||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
if (name == "version"_L1) continue;
|
||||||
|
QByteArray bytes = s.value(name).toByteArray();
|
||||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||||
CollectionModel::Grouping g;
|
CollectionModel::Grouping g;
|
||||||
ds >> g;
|
ds >> g;
|
||||||
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
|
ret->addAction(CreateGroupByAction(QUrl::fromPercentEncoding(name.toUtf8()), parent, g));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
QStringList saved = s.childKeys();
|
QStringList saved = s.childKeys();
|
||||||
for (int i = 0; i < saved.size(); ++i) {
|
for (int i = 0; i < saved.size(); ++i) {
|
||||||
if (saved.at(i) == "version"_L1) continue;
|
const QString &name = saved.at(i);
|
||||||
s.remove(saved.at(i));
|
if (name == "version"_L1) continue;
|
||||||
|
s.remove(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
@@ -339,7 +342,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
|||||||
|
|
||||||
if (!model_) return;
|
if (!model_) return;
|
||||||
|
|
||||||
QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
const QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||||
if (name.isEmpty()) return;
|
if (name.isEmpty()) return;
|
||||||
|
|
||||||
qLog(Debug) << "Saving current grouping to" << name;
|
qLog(Debug) << "Saving current grouping to" << name;
|
||||||
@@ -355,7 +358,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
|||||||
QDataStream datastream(&buffer, QIODevice::WriteOnly);
|
QDataStream datastream(&buffer, QIODevice::WriteOnly);
|
||||||
datastream << model_->GetGroupBy();
|
datastream << model_->GetGroupBy();
|
||||||
s.setValue("version", u"1"_s);
|
s.setValue("version", u"1"_s);
|
||||||
s.setValue(name, buffer);
|
s.setValue(QUrl::toPercentEncoding(name), buffer);
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
UpdateGroupByActions();
|
UpdateGroupByActions();
|
||||||
|
|||||||
@@ -135,4 +135,3 @@ class CollectionFilterWidget : public QWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // COLLECTIONFILTERWIDGET_H
|
#endif // COLLECTIONFILTERWIDGET_H
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -78,6 +78,8 @@ CollectionLibrary::CollectionLibrary(const SharedPtr<Database> database,
|
|||||||
|
|
||||||
model_ = new CollectionModel(backend_, albumcover_loader, this);
|
model_ = new CollectionModel(backend_, albumcover_loader, this);
|
||||||
|
|
||||||
|
full_rescan_revisions_[21] = tr("Support for sort tags artist, album, album artist, title, composer, and performer");
|
||||||
|
|
||||||
ReloadSettings();
|
ReloadSettings();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -122,6 +124,7 @@ void CollectionLibrary::Init() {
|
|||||||
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
|
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*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::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);
|
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 CollectionLibrary::SyncPlaycountAndRatingToFilesAsync() {
|
||||||
|
|
||||||
(void)QtConcurrent::run(&CollectionLibrary::SyncPlaycountAndRatingToFiles, this);
|
(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_) {
|
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_) {
|
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
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
#include <QMap>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
@@ -71,6 +72,7 @@ class CollectionLibrary : public QObject {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void SyncPlaycountAndRatingToFiles();
|
void SyncPlaycountAndRatingToFiles();
|
||||||
|
void SavePendingPlaycountsAndRatings();
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void ReloadSettings();
|
void ReloadSettings();
|
||||||
@@ -84,16 +86,26 @@ class CollectionLibrary : public QObject {
|
|||||||
|
|
||||||
void IncrementalScan();
|
void IncrementalScan();
|
||||||
|
|
||||||
|
void CurrentSongChanged(const Song &song);
|
||||||
|
void Stopped();
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void ExitReceived();
|
void ExitReceived();
|
||||||
void SongsPlaycountChanged(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) const;
|
void SongsRatingChanged(const SongList &songs, const bool save_tags = false);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void Error(const QString &error);
|
void Error(const QString &error);
|
||||||
void ExitFinished();
|
void ExitFinished();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
class PendingSongSave {
|
||||||
|
public:
|
||||||
|
Song song;
|
||||||
|
bool save_playcount = false;
|
||||||
|
bool save_rating = false;
|
||||||
|
};
|
||||||
|
|
||||||
const SharedPtr<TaskManager> task_manager_;
|
const SharedPtr<TaskManager> task_manager_;
|
||||||
const SharedPtr<TagReaderClient> tagreader_client_;
|
const SharedPtr<TagReaderClient> tagreader_client_;
|
||||||
|
|
||||||
@@ -111,6 +123,10 @@ class CollectionLibrary : public QObject {
|
|||||||
|
|
||||||
bool save_playcounts_to_files_;
|
bool save_playcounts_to_files_;
|
||||||
bool save_ratings_to_files_;
|
bool save_ratings_to_files_;
|
||||||
|
|
||||||
|
QUrl current_song_url_;
|
||||||
|
|
||||||
|
QMap<QUrl, SharedPtr<PendingSongSave>> pending_song_saves_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
|
|
||||||
#include "includes/scoped_ptr.h"
|
#include "includes/scoped_ptr.h"
|
||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "constants/collectionsettings.h"
|
||||||
#include "core/logging.h"
|
#include "core/logging.h"
|
||||||
#include "core/standardpaths.h"
|
#include "core/standardpaths.h"
|
||||||
#include "core/database.h"
|
#include "core/database.h"
|
||||||
@@ -71,12 +72,12 @@
|
|||||||
#include "covermanager/albumcoverloaderoptions.h"
|
#include "covermanager/albumcoverloaderoptions.h"
|
||||||
#include "covermanager/albumcoverloaderresult.h"
|
#include "covermanager/albumcoverloaderresult.h"
|
||||||
#include "covermanager/albumcoverloader.h"
|
#include "covermanager/albumcoverloader.h"
|
||||||
#include "constants/collectionsettings.h"
|
|
||||||
|
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
const int CollectionModel::kPrettyCoverSize = 32;
|
const int CollectionModel::kPrettyCoverSize = 32;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char kPixmapDiskCacheDir[] = "pixmapcache";
|
constexpr char kPixmapDiskCacheDir[] = "pixmapcache";
|
||||||
constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists");
|
constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists");
|
||||||
@@ -88,7 +89,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
|
|||||||
albumcover_loader_(albumcover_loader),
|
albumcover_loader_(albumcover_loader),
|
||||||
dir_model_(new CollectionDirectoryModel(backend, this)),
|
dir_model_(new CollectionDirectoryModel(backend, this)),
|
||||||
filter_(new CollectionFilter(this)),
|
filter_(new CollectionFilter(this)),
|
||||||
timer_reload_(new QTimer(this)),
|
|
||||||
timer_update_(new QTimer(this)),
|
timer_update_(new QTimer(this)),
|
||||||
icon_artist_(IconLoader::Load(u"folder-sound"_s)),
|
icon_artist_(IconLoader::Load(u"folder-sound"_s)),
|
||||||
use_disk_cache_(false),
|
use_disk_cache_(false),
|
||||||
@@ -130,10 +130,6 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
|
|||||||
backend_->UpdateTotalArtistCountAsync();
|
backend_->UpdateTotalArtistCountAsync();
|
||||||
backend_->UpdateTotalAlbumCountAsync();
|
backend_->UpdateTotalAlbumCountAsync();
|
||||||
|
|
||||||
timer_reload_->setSingleShot(true);
|
|
||||||
timer_reload_->setInterval(300ms);
|
|
||||||
QObject::connect(timer_reload_, &QTimer::timeout, this, &CollectionModel::Reload);
|
|
||||||
|
|
||||||
timer_update_->setSingleShot(false);
|
timer_update_->setSingleShot(false);
|
||||||
timer_update_->setInterval(20ms);
|
timer_update_->setInterval(20ms);
|
||||||
QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate);
|
QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate);
|
||||||
@@ -191,13 +187,9 @@ void CollectionModel::EndReset() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollectionModel::Reload() {
|
void CollectionModel::ResetInternal() {
|
||||||
|
|
||||||
loading_ = true;
|
loading_ = true;
|
||||||
if (timer_reload_->isActive()) {
|
|
||||||
timer_reload_->stop();
|
|
||||||
}
|
|
||||||
updates_.clear();
|
|
||||||
|
|
||||||
options_active_ = options_current_;
|
options_active_ = options_current_;
|
||||||
|
|
||||||
@@ -211,22 +203,16 @@ void CollectionModel::Reload() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollectionModel::ScheduleReset() {
|
|
||||||
|
|
||||||
if (!timer_reload_->isActive()) {
|
|
||||||
timer_reload_->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionModel::ReloadSettings() {
|
void CollectionModel::ReloadSettings() {
|
||||||
|
|
||||||
Settings settings;
|
Settings settings;
|
||||||
settings.beginGroup(CollectionSettings::kSettingsGroup);
|
settings.beginGroup(CollectionSettings::kSettingsGroup);
|
||||||
const bool show_pretty_covers = settings.value(CollectionSettings::kPrettyCovers, true).toBool();
|
const bool show_pretty_covers = settings.value(CollectionSettings::kPrettyCovers, true).toBool();
|
||||||
const bool show_dividers= settings.value(CollectionSettings::kShowDividers, true).toBool();
|
const bool show_dividers = settings.value(CollectionSettings::kShowDividers, true).toBool();
|
||||||
const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool();
|
const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool();
|
||||||
const bool sort_skips_articles = settings.value(CollectionSettings::kSortSkipsArticles, true).toBool();
|
const bool sort_skip_articles_for_artists = settings.value(CollectionSettings::kSkipArticlesForArtists, true).toBool();
|
||||||
|
const bool sort_skip_articles_for_albums = settings.value(CollectionSettings::kSkipArticlesForAlbums, false).toBool();
|
||||||
|
const bool use_sort_tags = settings.value(CollectionSettings::kUseSortTags, true).toBool();
|
||||||
|
|
||||||
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
|
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
|
||||||
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
|
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
|
||||||
@@ -241,11 +227,15 @@ void CollectionModel::ReloadSettings() {
|
|||||||
if (show_pretty_covers != options_current_.show_pretty_covers ||
|
if (show_pretty_covers != options_current_.show_pretty_covers ||
|
||||||
show_dividers != options_current_.show_dividers ||
|
show_dividers != options_current_.show_dividers ||
|
||||||
show_various_artists != options_current_.show_various_artists ||
|
show_various_artists != options_current_.show_various_artists ||
|
||||||
sort_skips_articles != options_current_.sort_skips_articles) {
|
sort_skip_articles_for_artists != options_current_.sort_skip_articles_for_artists ||
|
||||||
|
sort_skip_articles_for_albums != options_current_.sort_skip_articles_for_albums ||
|
||||||
|
use_sort_tags != options_current_.use_sort_tags) {
|
||||||
options_current_.show_pretty_covers = show_pretty_covers;
|
options_current_.show_pretty_covers = show_pretty_covers;
|
||||||
options_current_.show_dividers = show_dividers;
|
options_current_.show_dividers = show_dividers;
|
||||||
options_current_.show_various_artists = show_various_artists;
|
options_current_.show_various_artists = show_various_artists;
|
||||||
options_current_.sort_skips_articles = sort_skips_articles;
|
options_current_.sort_skip_articles_for_artists = sort_skip_articles_for_artists;
|
||||||
|
options_current_.sort_skip_articles_for_albums = sort_skip_articles_for_albums;
|
||||||
|
options_current_.use_sort_tags = use_sort_tags;
|
||||||
ScheduleReset();
|
ScheduleReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,10 +411,15 @@ void CollectionModel::RemoveSongs(const SongList &songs) {
|
|||||||
|
|
||||||
void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) {
|
void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) {
|
||||||
|
|
||||||
for (qint64 i = 0; i < songs.count(); i += 400LL) {
|
if (type == CollectionModelUpdate::Type::Reset) {
|
||||||
const qint64 number = std::min(songs.count() - i, 400LL);
|
updates_.enqueue(CollectionModelUpdate(type));
|
||||||
const SongList songs_to_queue = songs.mid(i, number);
|
}
|
||||||
updates_.enqueue(CollectionModelUpdate(type, songs_to_queue));
|
else {
|
||||||
|
for (qint64 i = 0; i < songs.count(); i += 400LL) {
|
||||||
|
const qint64 number = std::min(songs.count() - i, 400LL);
|
||||||
|
const SongList songs_to_queue = songs.mid(i, number);
|
||||||
|
updates_.enqueue(CollectionModelUpdate(type, songs_to_queue));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!timer_update_->isActive()) {
|
if (!timer_update_->isActive()) {
|
||||||
@@ -433,6 +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) {
|
void CollectionModel::ScheduleAddSongs(const SongList &songs) {
|
||||||
|
|
||||||
ScheduleUpdate(CollectionModelUpdate::Type::Add, songs);
|
ScheduleUpdate(CollectionModelUpdate::Type::Add, songs);
|
||||||
@@ -465,6 +468,9 @@ void CollectionModel::ProcessUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (update.type) {
|
switch (update.type) {
|
||||||
|
case CollectionModelUpdate::Type::Reset:
|
||||||
|
ResetInternal();
|
||||||
|
break;
|
||||||
case CollectionModelUpdate::Type::AddReAddOrUpdate:
|
case CollectionModelUpdate::Type::AddReAddOrUpdate:
|
||||||
AddReAddOrUpdateSongsInternal(update.songs);
|
AddReAddOrUpdateSongsInternal(update.songs);
|
||||||
break;
|
break;
|
||||||
@@ -539,7 +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
|
// Sanity check to make sure we don't add songs that are outside the user's filter
|
||||||
if (!options_active_.filter_options.Matches(song)) continue;
|
if (!options_active_.filter_options.Matches(song)) continue;
|
||||||
|
|
||||||
if (song_nodes_.contains(song.id())) continue;
|
if (song_nodes_.contains(song.id())) {
|
||||||
|
qLog(Debug) << song.id() << song.title() << "already exists, skipping";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Before we can add each song we need to make sure the required container items already exist in the tree.
|
// Before we can add each song we need to make sure the required container items already exist in the tree.
|
||||||
// These depend on which "group by" settings the user has on the collection.
|
// These depend on which "group by" settings the user has on the collection.
|
||||||
@@ -680,8 +689,8 @@ void CollectionModel::RemoveSongsInternal(const SongList &songs) {
|
|||||||
if (!divider_nodes_.contains(divider_key)) continue;
|
if (!divider_nodes_.contains(divider_key)) continue;
|
||||||
|
|
||||||
// Look to see if there are any other items still under this divider
|
// Look to see if there are any other items still under this divider
|
||||||
QList<CollectionItem*> container_nodes = container_nodes_[0].values();
|
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; })) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,7 +708,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
|||||||
|
|
||||||
QString divider_key;
|
QString divider_key;
|
||||||
if (options_active_.show_dividers && container_level == 0) {
|
if (options_active_.show_dividers && container_level == 0) {
|
||||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skips_articles));
|
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums, options_active_.use_sort_tags));
|
||||||
if (!divider_key.isEmpty()) {
|
if (!divider_key.isEmpty()) {
|
||||||
if (!divider_nodes_.contains(divider_key)) {
|
if (!divider_nodes_.contains(divider_key)) {
|
||||||
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
|
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
|
||||||
@@ -713,7 +722,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
|||||||
item->container_level = container_level;
|
item->container_level = container_level;
|
||||||
item->container_key = container_key;
|
item->container_key = container_key;
|
||||||
item->display_text = DisplayText(group_by, song);
|
item->display_text = DisplayText(group_by, song);
|
||||||
item->sort_text = SortText(group_by, song, options_active_.sort_skips_articles);
|
item->sort_text = SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums, options_active_.use_sort_tags);
|
||||||
if (!divider_key.isEmpty()) {
|
if (!divider_key.isEmpty()) {
|
||||||
item->sort_text.prepend(divider_key + QLatin1Char(' '));
|
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) {
|
switch (group_by) {
|
||||||
case GroupBy::AlbumArtist:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
case GroupBy::Disc:
|
||||||
return SortTextForNumber(std::max(0, song.disc()));
|
return SortTextForNumber(std::max(0, song.disc()));
|
||||||
case GroupBy::Year:
|
case GroupBy::Year:
|
||||||
return SortTextForNumber(std::max(0, song.year())) + QLatin1Char(' ');
|
return SortTextForYear(song.year()) + QLatin1Char(' ');
|
||||||
case GroupBy::OriginalYear:
|
case GroupBy::OriginalYear:
|
||||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
|
return SortTextForYear(song.effective_originalyear()) + QLatin1Char(' ');
|
||||||
case GroupBy::Genre:
|
case GroupBy::Genre:
|
||||||
return SortTextForArtist(song.genre(), sort_skips_articles);
|
return SortText(song.genre());
|
||||||
case GroupBy::Composer:
|
case GroupBy::Composer:
|
||||||
return SortTextForArtist(song.composer(), sort_skips_articles);
|
return SortTextForName(use_sort_tags ? song.effective_composersort() : song.composer(), sort_skip_articles_for_artists);
|
||||||
case GroupBy::Performer:
|
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:
|
case GroupBy::Grouping:
|
||||||
return SortTextForArtist(song.grouping(), sort_skips_articles);
|
return SortText(song.grouping());
|
||||||
case GroupBy::FileType:
|
case GroupBy::FileType:
|
||||||
return song.TextForFiletype();
|
return song.TextForFiletype();
|
||||||
case GroupBy::Format:
|
case GroupBy::Format:
|
||||||
@@ -1135,21 +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);
|
return sort_skip_articles ? SkipArticles(SortText(name)) : SortText(name);
|
||||||
|
|
||||||
if (skip_articles) {
|
|
||||||
for (const auto &i : Song::kArticles) {
|
|
||||||
if (artist.startsWith(i)) {
|
|
||||||
qint64 ilen = i.length();
|
|
||||||
artist = artist.right(artist.length() - ilen) + ", "_L1 + i.left(ilen - 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return artist;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1168,18 +1165,32 @@ QString CollectionModel::SortTextForSong(const Song &song) {
|
|||||||
|
|
||||||
QString CollectionModel::SortTextForYear(const int year) {
|
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;
|
return QStringLiteral("0").repeated(qMax(0, 4 - str.length())) + str;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString CollectionModel::SortTextForBitrate(const int bitrate) {
|
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;
|
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) {
|
bool CollectionModel::IsSongTitleDataChanged(const Song &song1, const Song &song2) {
|
||||||
|
|
||||||
return song1.url() != song2.url() ||
|
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());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
case GroupBy::AlbumDisc:
|
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 (!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());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
case GroupBy::YearAlbum:
|
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 (!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());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
case GroupBy::YearAlbumDisc:
|
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 (!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());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
case GroupBy::OriginalYearAlbum:
|
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 (!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());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
case GroupBy::OriginalYearAlbumDisc:
|
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 (!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());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
@@ -1238,10 +1252,10 @@ QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song,
|
|||||||
key = PrettyDisc(song.disc());
|
key = PrettyDisc(song.disc());
|
||||||
break;
|
break;
|
||||||
case GroupBy::Year:
|
case GroupBy::Year:
|
||||||
key = QString::number(std::max(0, song.year()));
|
key = SortTextForYear(song.year());
|
||||||
break;
|
break;
|
||||||
case GroupBy::OriginalYear:
|
case GroupBy::OriginalYear:
|
||||||
key = QString::number(std::max(0, song.effective_originalyear()));
|
key = SortTextForYear(song.effective_originalyear());
|
||||||
break;
|
break;
|
||||||
case GroupBy::Genre:
|
case GroupBy::Genre:
|
||||||
key = TextOrUnknown(song.genre());
|
key = TextOrUnknown(song.genre());
|
||||||
@@ -1333,7 +1347,7 @@ QString CollectionModel::DividerKey(const GroupBy group_by, const Song &song, co
|
|||||||
case GroupBy::Bitdepth:
|
case GroupBy::Bitdepth:
|
||||||
return SortTextForNumber(song.bitdepth());
|
return SortTextForNumber(song.bitdepth());
|
||||||
case GroupBy::Bitrate:
|
case GroupBy::Bitrate:
|
||||||
return SortTextForNumber(song.bitrate());
|
return SortTextForBitrate(song.bitrate());
|
||||||
case GroupBy::None:
|
case GroupBy::None:
|
||||||
case GroupBy::GroupByCount:
|
case GroupBy::GroupByCount:
|
||||||
return QString();
|
return QString();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -129,14 +129,18 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||||||
show_dividers(true),
|
show_dividers(true),
|
||||||
show_pretty_covers(true),
|
show_pretty_covers(true),
|
||||||
show_various_artists(true),
|
show_various_artists(true),
|
||||||
sort_skips_articles(true),
|
sort_skip_articles_for_artists(false),
|
||||||
|
sort_skip_articles_for_albums(false),
|
||||||
|
use_sort_tags(true),
|
||||||
separate_albums_by_grouping(false) {}
|
separate_albums_by_grouping(false) {}
|
||||||
|
|
||||||
Grouping group_by;
|
Grouping group_by;
|
||||||
bool show_dividers;
|
bool show_dividers;
|
||||||
bool show_pretty_covers;
|
bool show_pretty_covers;
|
||||||
bool show_various_artists;
|
bool show_various_artists;
|
||||||
bool sort_skips_articles;
|
bool sort_skip_articles_for_artists;
|
||||||
|
bool sort_skip_articles_for_albums;
|
||||||
|
bool use_sort_tags;
|
||||||
bool separate_albums_by_grouping;
|
bool separate_albums_by_grouping;
|
||||||
CollectionFilterOptions filter_options;
|
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 PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
||||||
static QString PrettyDisc(const int disc);
|
static QString PrettyDisc(const int disc);
|
||||||
static QString PrettyFormat(const Song &song);
|
static QString PrettyFormat(const Song &song);
|
||||||
QString SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles);
|
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 SortText(QString text);
|
||||||
|
static QString SortTextForName(const QString &name, const bool sort_skip_articles);
|
||||||
static QString SortTextForNumber(const int number);
|
static QString SortTextForNumber(const int number);
|
||||||
static QString SortTextForArtist(QString artist, const bool skip_articles);
|
|
||||||
static QString SortTextForSong(const Song &song);
|
static QString SortTextForSong(const Song &song);
|
||||||
static QString SortTextForYear(const int year);
|
static QString SortTextForYear(const int year);
|
||||||
static QString SortTextForBitrate(const int bitrate);
|
static QString SortTextForBitrate(const int bitrate);
|
||||||
|
static QString SkipArticles(QString name);
|
||||||
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
|
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
|
||||||
QString ContainerKey(const GroupBy group_by, const Song &song, bool &has_unique_album_identifier) const;
|
QString ContainerKey(const GroupBy group_by, const Song &song, bool &has_unique_album_identifier) const;
|
||||||
|
|
||||||
@@ -228,7 +233,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||||||
|
|
||||||
QVariant data(CollectionItem *item, const int role) const;
|
QVariant data(CollectionItem *item, const int role) const;
|
||||||
|
|
||||||
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
|
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs = SongList());
|
||||||
void ScheduleAddSongs(const SongList &songs);
|
void ScheduleAddSongs(const SongList &songs);
|
||||||
void ScheduleUpdateSongs(const SongList &songs);
|
void ScheduleUpdateSongs(const SongList &songs);
|
||||||
void ScheduleRemoveSongs(const SongList &songs);
|
void ScheduleRemoveSongs(const SongList &songs);
|
||||||
@@ -259,7 +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);
|
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void Reload();
|
void ResetInternal();
|
||||||
void ScheduleReset();
|
void ScheduleReset();
|
||||||
void ProcessUpdate();
|
void ProcessUpdate();
|
||||||
void LoadSongsFromSqlAsyncFinished();
|
void LoadSongsFromSqlAsyncFinished();
|
||||||
@@ -278,7 +283,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||||||
const SharedPtr<AlbumCoverLoader> albumcover_loader_;
|
const SharedPtr<AlbumCoverLoader> albumcover_loader_;
|
||||||
CollectionDirectoryModel *dir_model_;
|
CollectionDirectoryModel *dir_model_;
|
||||||
CollectionFilter *filter_;
|
CollectionFilter *filter_;
|
||||||
QTimer *timer_reload_;
|
|
||||||
QTimer *timer_update_;
|
QTimer *timer_update_;
|
||||||
|
|
||||||
QPixmap pixmap_no_cover_;
|
QPixmap pixmap_no_cover_;
|
||||||
|
|||||||
@@ -25,12 +25,13 @@
|
|||||||
class CollectionModelUpdate {
|
class CollectionModelUpdate {
|
||||||
public:
|
public:
|
||||||
enum class Type {
|
enum class Type {
|
||||||
|
Reset,
|
||||||
AddReAddOrUpdate,
|
AddReAddOrUpdate,
|
||||||
Add,
|
Add,
|
||||||
Update,
|
Update,
|
||||||
Remove,
|
Remove,
|
||||||
};
|
};
|
||||||
explicit CollectionModelUpdate(const Type _type, const SongList &_songs);
|
explicit CollectionModelUpdate(const Type _type, const SongList &_songs = SongList());
|
||||||
Type type;
|
Type type;
|
||||||
SongList songs;
|
SongList songs;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ CollectionPlaylistItem::CollectionPlaylistItem(const Song::Source source) : Play
|
|||||||
|
|
||||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
|
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
|
||||||
|
|
||||||
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
|
|
||||||
|
|
||||||
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
|
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
|
||||||
|
|
||||||
int col = 0;
|
int col = 0;
|
||||||
@@ -62,7 +60,7 @@ void CollectionPlaylistItem::Reload() {
|
|||||||
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
|
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UpdateTemporaryMetadata(song_);
|
UpdateStreamMetadata(song_);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -78,16 +76,9 @@ QVariant CollectionPlaylistItem::DatabaseValue(const DatabaseColumn database_col
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Song CollectionPlaylistItem::Metadata() const {
|
|
||||||
|
|
||||||
if (HasTemporaryMetadata()) return temp_metadata_;
|
|
||||||
return song_;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void CollectionPlaylistItem::SetArtManual(const QUrl &cover_url) {
|
void CollectionPlaylistItem::SetArtManual(const QUrl &cover_url) {
|
||||||
|
|
||||||
song_.set_art_manual(cover_url);
|
song_.set_art_manual(cover_url);
|
||||||
if (HasTemporaryMetadata()) temp_metadata_.set_art_manual(cover_url);
|
if (HasStreamMetadata()) stream_song_.set_art_manual(cover_url);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,19 +35,17 @@ class CollectionPlaylistItem : public PlaylistItem {
|
|||||||
explicit CollectionPlaylistItem(const Song::Source source);
|
explicit CollectionPlaylistItem(const Song::Source source);
|
||||||
explicit CollectionPlaylistItem(const Song &song);
|
explicit CollectionPlaylistItem(const Song &song);
|
||||||
|
|
||||||
QUrl Url() const override;
|
Song OriginalMetadata() const override { return song_; }
|
||||||
|
void SetOriginalMetadata(const Song &song) override { song_ = song; }
|
||||||
|
|
||||||
|
QUrl OriginalUrl() const override { return song_.url(); }
|
||||||
|
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
|
||||||
|
|
||||||
bool InitFromQuery(const SqlRow &query) override;
|
bool InitFromQuery(const SqlRow &query) override;
|
||||||
void Reload() override;
|
void Reload() override;
|
||||||
|
|
||||||
Song Metadata() const override;
|
|
||||||
Song OriginalMetadata() const override { return song_; }
|
|
||||||
void SetMetadata(const Song &song) override { song_ = song; }
|
|
||||||
|
|
||||||
void SetArtManual(const QUrl &cover_url) override;
|
void SetArtManual(const QUrl &cover_url) override;
|
||||||
|
|
||||||
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QVariant DatabaseValue(const DatabaseColumn database_column) const override;
|
QVariant DatabaseValue(const DatabaseColumn database_column) const override;
|
||||||
Song DatabaseSongMetadata() const override { return Song(source_); }
|
Song DatabaseSongMetadata() const override { return Song(source_); }
|
||||||
@@ -60,4 +58,3 @@ class CollectionPlaylistItem : public PlaylistItem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // COLLECTIONPLAYLISTITEM_H
|
#endif // COLLECTIONPLAYLISTITEM_H
|
||||||
|
|
||||||
|
|||||||
@@ -101,9 +101,9 @@ void CollectionQuery::AddCompilationRequirement(const bool compilation) {
|
|||||||
QString CollectionQuery::GetInnerQuery() const {
|
QString CollectionQuery::GetInnerQuery() const {
|
||||||
return duplicates_only_
|
return duplicates_only_
|
||||||
? QStringLiteral(" INNER JOIN (select * from duplicated_songs) dsongs "
|
? QStringLiteral(" INNER JOIN (select * from duplicated_songs) dsongs "
|
||||||
"ON (%songs_table.artist = dsongs.dup_artist "
|
"ON (%songs_table.artist = dsongs.dup_artist "
|
||||||
"AND %songs_table.album = dsongs.dup_album "
|
"AND %songs_table.album = dsongs.dup_album "
|
||||||
"AND %songs_table.title = dsongs.dup_title) ")
|
"AND %songs_table.title = dsongs.dup_title) ")
|
||||||
: QString();
|
: QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,10 +65,8 @@
|
|||||||
#include "collectionitem.h"
|
#include "collectionitem.h"
|
||||||
#include "collectionitemdelegate.h"
|
#include "collectionitemdelegate.h"
|
||||||
#include "collectionview.h"
|
#include "collectionview.h"
|
||||||
#ifndef Q_OS_WIN32
|
#include "device/devicemanager.h"
|
||||||
# include "device/devicemanager.h"
|
#include "device/devicestatefiltermodel.h"
|
||||||
# include "device/devicestatefiltermodel.h"
|
|
||||||
#endif
|
|
||||||
#include "dialogs/edittagdialog.h"
|
#include "dialogs/edittagdialog.h"
|
||||||
#include "dialogs/deleteconfirmationdialog.h"
|
#include "dialogs/deleteconfirmationdialog.h"
|
||||||
#include "organize/organizedialog.h"
|
#include "organize/organizedialog.h"
|
||||||
@@ -95,9 +93,7 @@ CollectionView::CollectionView(QWidget *parent)
|
|||||||
action_open_in_new_playlist_(nullptr),
|
action_open_in_new_playlist_(nullptr),
|
||||||
action_organize_(nullptr),
|
action_organize_(nullptr),
|
||||||
action_search_for_this_(nullptr),
|
action_search_for_this_(nullptr),
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
action_copy_to_device_(nullptr),
|
action_copy_to_device_(nullptr),
|
||||||
#endif
|
|
||||||
action_edit_track_(nullptr),
|
action_edit_track_(nullptr),
|
||||||
action_edit_tracks_(nullptr),
|
action_edit_tracks_(nullptr),
|
||||||
action_rescan_songs_(nullptr),
|
action_rescan_songs_(nullptr),
|
||||||
@@ -387,7 +383,7 @@ void CollectionView::keyPressEvent(QKeyEvent *e) {
|
|||||||
case Qt::Key_Enter:
|
case Qt::Key_Enter:
|
||||||
case Qt::Key_Return:
|
case Qt::Key_Return:
|
||||||
if (currentIndex().isValid()) {
|
if (currentIndex().isValid()) {
|
||||||
AddToPlaylist();
|
Q_EMIT doubleClicked(currentIndex());
|
||||||
}
|
}
|
||||||
e->accept();
|
e->accept();
|
||||||
break;
|
break;
|
||||||
@@ -417,9 +413,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
|
|
||||||
context_menu_->addSeparator();
|
context_menu_->addSeparator();
|
||||||
action_organize_ = context_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &CollectionView::Organize);
|
action_organize_ = context_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &CollectionView::Organize);
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &CollectionView::CopyToDevice);
|
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &CollectionView::CopyToDevice);
|
||||||
#endif
|
|
||||||
action_delete_files_ = context_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &CollectionView::Delete);
|
action_delete_files_ = context_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &CollectionView::Delete);
|
||||||
|
|
||||||
context_menu_->addSeparator();
|
context_menu_->addSeparator();
|
||||||
@@ -439,10 +433,8 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
|
|
||||||
context_menu_->addMenu(filter_widget_->menu());
|
context_menu_->addMenu(filter_widget_->menu());
|
||||||
|
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
action_copy_to_device_->setDisabled(device_manager_->connected_devices_model()->rowCount() == 0);
|
action_copy_to_device_->setDisabled(device_manager_->connected_devices_model()->rowCount() == 0);
|
||||||
QObject::connect(device_manager_->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, action_copy_to_device_, &QAction::setDisabled);
|
QObject::connect(device_manager_->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, action_copy_to_device_, &QAction::setDisabled);
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,9 +473,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
action_rescan_songs_->setEnabled(regular_editable > 0);
|
action_rescan_songs_->setEnabled(regular_editable > 0);
|
||||||
|
|
||||||
action_organize_->setVisible(regular_elements == regular_editable);
|
action_organize_->setVisible(regular_elements == regular_editable);
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
action_copy_to_device_->setVisible(regular_elements == regular_editable);
|
action_copy_to_device_->setVisible(regular_elements == regular_editable);
|
||||||
#endif
|
|
||||||
|
|
||||||
action_delete_files_->setVisible(delete_files_);
|
action_delete_files_->setVisible(delete_files_);
|
||||||
|
|
||||||
@@ -492,9 +482,7 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
|
|
||||||
// only when all selected items are editable
|
// only when all selected items are editable
|
||||||
action_organize_->setEnabled(regular_elements == regular_editable);
|
action_organize_->setEnabled(regular_elements == regular_editable);
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
|
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
|
||||||
#endif
|
|
||||||
|
|
||||||
action_delete_files_->setEnabled(delete_files_);
|
action_delete_files_->setEnabled(delete_files_);
|
||||||
|
|
||||||
@@ -759,7 +747,6 @@ void CollectionView::RescanSongs() {
|
|||||||
|
|
||||||
void CollectionView::CopyToDevice() {
|
void CollectionView::CopyToDevice() {
|
||||||
|
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
if (!organize_dialog_) {
|
if (!organize_dialog_) {
|
||||||
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
|
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
|
||||||
}
|
}
|
||||||
@@ -768,7 +755,6 @@ void CollectionView::CopyToDevice() {
|
|||||||
organize_dialog_->SetCopy(true);
|
organize_dialog_->SetCopy(true);
|
||||||
organize_dialog_->SetSongs(GetSelectedSongs());
|
organize_dialog_->SetSongs(GetSelectedSongs());
|
||||||
organize_dialog_->show();
|
organize_dialog_->show();
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ class CollectionView : public AutoExpandingTreeView {
|
|||||||
void DeleteFilesFinished(const SongList &songs_with_errors);
|
void DeleteFilesFinished(const SongList &songs_with_errors);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void RecheckIsEmpty();
|
|
||||||
void SetShowInVarious(const bool on);
|
void SetShowInVarious(const bool on);
|
||||||
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
|
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
|
||||||
void SaveContainerPath(const QModelIndex &child);
|
void SaveContainerPath(const QModelIndex &child);
|
||||||
@@ -176,9 +175,7 @@ class CollectionView : public AutoExpandingTreeView {
|
|||||||
QAction *action_organize_;
|
QAction *action_organize_;
|
||||||
QAction *action_search_for_this_;
|
QAction *action_search_for_this_;
|
||||||
|
|
||||||
#ifndef Q_OS_WIN32
|
|
||||||
QAction *action_copy_to_device_;
|
QAction *action_copy_to_device_;
|
||||||
#endif
|
|
||||||
QAction *action_edit_track_;
|
QAction *action_edit_track_;
|
||||||
QAction *action_edit_tracks_;
|
QAction *action_edit_tracks_;
|
||||||
QAction *action_rescan_songs_;
|
QAction *action_rescan_songs_;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user