Compare commits
233 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 |
@@ -130,7 +130,10 @@ InsertBraces: false
|
||||
InsertTrailingCommas: None
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
KeepEmptyLines:
|
||||
AtEndOfFile: true
|
||||
AtStartOfBlock: true
|
||||
AtStartOfFile: true
|
||||
LambdaBodyIndentation: Signature
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
|
||||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,4 +0,0 @@
|
||||
github: jonaski
|
||||
patreon: jonaskvinge
|
||||
ko_fi: jonaskvinge
|
||||
custom: https://paypal.me/jonaskvinge
|
||||
119
.github/workflows/build.yml
vendored
119
.github/workflows/build.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
opensuse_version: [ 'tumbleweed', 'leap:15.6' ]
|
||||
opensuse_version: [ 'tumbleweed', 'leap:15.6', 'leap:16.0' ]
|
||||
container:
|
||||
image: opensuse/${{matrix.opensuse_version}}
|
||||
steps:
|
||||
@@ -27,11 +27,11 @@ jobs:
|
||||
- name: Upgrade packages (Leap)
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
run: zypper -n --gpg-auto-import-keys up
|
||||
- name: Install gcc (Tumbleweed)
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
- name: Install gcc
|
||||
if: matrix.opensuse_version != 'leap:15.6'
|
||||
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
|
||||
- name: Install gcc (Leap)
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
- name: Install gcc (leap:15.6)
|
||||
if: matrix.opensuse_version == 'leap:15.6'
|
||||
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
|
||||
- name: Install packages
|
||||
run: >
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
- name: Install kdsingleapplication-qt6-devel
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
if: matrix.opensuse_version != 'leap:15.6'
|
||||
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.opensuse_version == 'leap:15.6'
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -115,14 +115,14 @@ jobs:
|
||||
- name: Copy source tarball
|
||||
working-directory: build
|
||||
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
|
||||
- name: Build RPM (Tumbleweed)
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
- name: Build RPM
|
||||
if: matrix.opensuse_version != 'leap:15.6'
|
||||
env:
|
||||
RPM_BUILD_NCPUS: 4
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
- name: Build RPM (Leap)
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
- name: Build RPM (leap:15.6)
|
||||
if: matrix.opensuse_version == 'leap:15.6'
|
||||
env:
|
||||
RPM_BUILD_NCPUS: 4
|
||||
CC: gcc-14
|
||||
@@ -134,14 +134,14 @@ jobs:
|
||||
run: echo "subdir=$(echo ${{matrix.opensuse_version}} | sed 's/leap:/lp/g' | sed 's/\.//g')" > $GITHUB_OUTPUT
|
||||
- name: Upload source
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: source
|
||||
path: |
|
||||
/usr/src/packages/SOURCES/*.xz
|
||||
- name: Upload rpm
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: opensuse-${{steps.set-subdir.outputs.subdir}}
|
||||
path: |
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
fedora_version: [ '41', '42', '43' ]
|
||||
fedora_version: [ '42', '43', '44' ]
|
||||
container:
|
||||
image: fedora:${{matrix.fedora_version}}
|
||||
steps:
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: fedora-${{matrix.fedora_version}}
|
||||
path: |
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
- name: Remove files
|
||||
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -333,7 +333,7 @@ jobs:
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
- name: Upload artifacts
|
||||
if: matrix.openmandriva_version != 'cooker'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: openmandriva-${{matrix.openmandriva_version}}
|
||||
path: |
|
||||
@@ -409,7 +409,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -434,7 +434,7 @@ jobs:
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: mageia-${{matrix.mageia_version}}
|
||||
path: |
|
||||
@@ -449,7 +449,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
debian_version: [ 'bookworm', 'trixie' ]
|
||||
debian_version: [ 'bookworm', 'trixie', 'forky' ]
|
||||
container:
|
||||
image: debian:${{matrix.debian_version}}
|
||||
steps:
|
||||
@@ -499,7 +499,11 @@ jobs:
|
||||
qt6-tools-dev-tools
|
||||
qt6-l10n-tools
|
||||
rapidjson-dev
|
||||
- name: Install KDSingleApplication
|
||||
if: matrix.debian_version != 'bookworm'
|
||||
run: apt install -y libkdsingleapplication-qt6-dev
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.debian_version == 'bookworm'
|
||||
run: |
|
||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||
cd KDSingleApplication
|
||||
@@ -507,7 +511,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -524,7 +528,7 @@ jobs:
|
||||
- name: Copy deb
|
||||
run: cp ../*.deb .
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: debian-${{matrix.debian_version}}
|
||||
path: |
|
||||
@@ -538,7 +542,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'plucky' ]
|
||||
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -591,7 +595,11 @@ jobs:
|
||||
qt6-tools-dev-tools
|
||||
qt6-l10n-tools
|
||||
rapidjson-dev
|
||||
- name: Install KDSingleApplication
|
||||
if: matrix.ubuntu_version != 'noble'
|
||||
run: apt install -y libkdsingleapplication-qt6-dev
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.ubuntu_version == 'noble'
|
||||
run: |
|
||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||
cd KDSingleApplication
|
||||
@@ -599,7 +607,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -616,7 +624,7 @@ jobs:
|
||||
- name: Copy deb
|
||||
run: cp ../*.deb ../*.ddeb .
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ubuntu-${{matrix.ubuntu_version}}
|
||||
path: |
|
||||
@@ -631,7 +639,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'plucky' ]
|
||||
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -691,7 +699,7 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y keyboxd
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -727,16 +735,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Free disk space
|
||||
run: |
|
||||
df -h
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
|
||||
sudo apt-get clean
|
||||
df -h
|
||||
- name: Build FreeBSD
|
||||
id: build-freebsd
|
||||
uses: vmactions/freebsd-vm@v1.2.3
|
||||
uses: vmactions/freebsd-vm@v1.3.7
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
mem: 8192
|
||||
prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio sparsehash rapidjson
|
||||
run: |
|
||||
set -e
|
||||
@@ -752,13 +766,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Build OpenBSD
|
||||
id: build-openbsd
|
||||
uses: vmactions/openbsd-vm@v1.2.0
|
||||
uses: vmactions/openbsd-vm@v1.3.1
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
@@ -779,7 +793,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ 'macos-13', 'macos-15' ]
|
||||
runner: [ 'macos-15-intel', 'macos-15' ]
|
||||
buildtype: [ 'release' ]
|
||||
|
||||
runs-on: ${{ matrix.runner }}
|
||||
@@ -818,14 +832,14 @@ jobs:
|
||||
rm -f uninstall.sh
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Import certificate file
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false
|
||||
uses: apple-actions/import-codesign-certs@v5
|
||||
uses: apple-actions/import-codesign-certs@v6
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE }}
|
||||
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
|
||||
@@ -882,9 +896,9 @@ jobs:
|
||||
run: make deploy
|
||||
|
||||
- name: Manually Codesign
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-13'
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
|
||||
working-directory: build
|
||||
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
||||
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib,libbrotlidec.1.dylib,libsoup-3.0.0.dylib,libnghttp2.14.dylib,libpsl.5.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
||||
|
||||
- name: Manually Codesign
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
|
||||
@@ -969,7 +983,7 @@ jobs:
|
||||
run: echo "cmake_buildtype=$(echo ${{env.buildtype}} | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}')" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1072,7 +1086,7 @@ jobs:
|
||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1302,7 +1316,7 @@ jobs:
|
||||
- name: Set SDK version
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
run: echo "sdk_version=10.0.19041.0" >> $GITHUB_ENV
|
||||
run: echo "sdk_version=10.0.22621.0" >> $GITHUB_ENV
|
||||
|
||||
- name: Set SDK version
|
||||
if: matrix.arch == 'arm64'
|
||||
@@ -1337,7 +1351,9 @@ jobs:
|
||||
|
||||
- name: Copy bin files
|
||||
shell: bash
|
||||
run: cp /c/strawberry/c/bin/{patch.exe,strip.exe,strings.exe,objdump.exe} ${{env.prefix_path_unix}}/bin
|
||||
run: |
|
||||
cp /c/mingw64/bin/{strip.exe,strings.exe,objdump.exe} ${{env.prefix_path_unix}}/bin
|
||||
cp /c/strawberry/c/bin/patch.exe ${{env.prefix_path_unix}}/bin
|
||||
|
||||
- name: Delete conflicting files
|
||||
shell: bash
|
||||
@@ -1395,7 +1411,7 @@ jobs:
|
||||
vsversion: 2022
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1507,14 +1523,11 @@ jobs:
|
||||
run: copy ${{env.prefix_path_backslash}}\lib\gstreamer-1.0\*.dll .\gstreamer-plugins\
|
||||
|
||||
- name: Download copydlldeps.sh
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: curl -f -O -L https://raw.githubusercontent.com/strawberrymusicplayer/strawberry-mxe/master/tools/copydlldeps.sh
|
||||
|
||||
- name: Copy dependencies
|
||||
# copydlldeps.sh doesn't work with arm64 binaries.
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: >
|
||||
@@ -1531,12 +1544,6 @@ jobs:
|
||||
-F ./gstreamer-plugins
|
||||
-R ${{env.prefix_path_unix}}/bin
|
||||
|
||||
- name: Copy dependencies
|
||||
if: matrix.arch == 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: cp -v ${{env.prefix_path_unix}}/bin/{avcodec*.dll,avfilter*.dll,avformat*.dll,avutil*.dll,brotlicommon.dll,brotlidec.dll,chromaprint.dll,ebur128.dll,faad-2.dll,fdk-aac.dll,ffi-7.dll,FLAC.dll,freetype*.dll,getopt.dll,gio-2.0-0.dll,glib-2.0-0.dll,gme.dll,gmodule-2.0-0.dll,gobject-2.0-0.dll,gst-discoverer-1.0.exe,gst-launch-1.0.exe,gst-play-1.0.exe,gstadaptivedemux-1.0-0.dll,gstapp-1.0-0.dll,gstaudio-1.0-0.dll,gstbadaudio-1.0-0.dll,gstbase-1.0-0.dll,gstcodecparsers-1.0-0.dll,gstfft-1.0-0.dll,gstisoff-1.0-0.dll,gstmpegts-1.0-0.dll,gstnet-1.0-0.dll,gstpbutils-1.0-0.dll,gstreamer-1.0-0.dll,gstriff-1.0-0.dll,gstrtp-1.0-0.dll,gstrtsp-1.0-0.dll,gstsdp-1.0-0.dll,gsttag-1.0-0.dll,gsturidownloader-1.0-0.dll,gstvideo-1.0-0.dll,gstwinrt-1.0-0.dll,harfbuzz.dll,icudt*.dll,icuin*.dll,icuuc*.dll,intl-8.dll,jpeg62.dll,kdsingleapplication*.dll,libbs2b.dll,libcrypto-3-*.dll,fftw3.dll,libiconv*.dll,liblzma.dll,libmp3lame.dll,libopenmpt.dll,libpng16*.dll,libspeex*.dll,libssl-3-*.dll,libxml2*.dll,mpcdec.dll,mpg123.dll,nghttp2.dll,ogg.dll,opus.dll,orc-0.4-0.dll,pcre2-16*.dll,pcre2-8*.dll,postproc*.dll,psl-5.dll,Qt6Concurrent*.dll,Qt6Core*.dll,Qt6Gui*.dll,Qt6Network*.dll,Qt6Sql*.dll,Qt6Widgets*.dll,qtsparkle-qt6.dll,soup-3.0-0.dll,sqlite3.dll,sqlite3.exe,swresample*.dll,swscale*.dll,tag.dll,vorbis.dll,vorbisfile.dll,wavpackdll.dll,zlib*.dll} .
|
||||
|
||||
- name: Copy nsis files
|
||||
shell: cmd
|
||||
working-directory: build
|
||||
@@ -1637,11 +1644,11 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: sudo apt install -y git rsync
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
- name: SSH Setup
|
||||
@@ -1685,7 +1692,7 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: sudo apt install -y git jq gh
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Show release assets
|
||||
@@ -1693,7 +1700,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Add artifacts to release
|
||||
|
||||
33
.gitignore
vendored
33
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
/build
|
||||
/bin
|
||||
/CMakeLists.txt.user
|
||||
/.qtcreator
|
||||
/.kdev4
|
||||
/strawberry.kdev4
|
||||
/.vscode
|
||||
@@ -12,3 +12,34 @@
|
||||
/CMakeSettings.json
|
||||
/dist/scripts/maketarball.sh
|
||||
/debian/changelog
|
||||
_codeql_detected_source_root
|
||||
|
||||
# Build output (keep build tooling scripts in /build_tools/ tracked)
|
||||
/cmake-build*/
|
||||
/build*/
|
||||
!/build_tools/
|
||||
!/build_tools/**
|
||||
|
||||
# macOS noise
|
||||
.DS_Store
|
||||
/bin
|
||||
/CMakeLists.txt.user
|
||||
/.qtcreator
|
||||
/.kdev4
|
||||
/strawberry.kdev4
|
||||
/.vscode
|
||||
/.code-workspace
|
||||
/.sublime-workspace
|
||||
/.idea
|
||||
/.vs
|
||||
/out
|
||||
/CMakeSettings.json
|
||||
/dist/scripts/maketarball.sh
|
||||
/debian/changelog
|
||||
_codeql_detected_source_root
|
||||
|
||||
# Build output (keep build tooling scripts in /build_tools/ tracked)
|
||||
/cmake-build*/
|
||||
/build*/
|
||||
!/build_tools/
|
||||
!/build_tools/**
|
||||
|
||||
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")
|
||||
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)
|
||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||
endif()
|
||||
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
if(TARGET RapidJSON::RapidJSON)
|
||||
target_link_libraries(discord-rpc PRIVATE RapidJSON::RapidJSON)
|
||||
elseif(RapidJSON_INCLUDE_DIRS)
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
endif()
|
||||
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
2
3rdparty/discord-rpc/discord_connection.h
vendored
2
3rdparty/discord-rpc/discord_connection.h
vendored
@@ -35,7 +35,7 @@ int GetProcessId();
|
||||
|
||||
struct BaseConnection {
|
||||
static BaseConnection *Create();
|
||||
static void Destroy(BaseConnection *&);
|
||||
static void Destroy(BaseConnection*&);
|
||||
bool isOpen = false;
|
||||
bool Open();
|
||||
bool Close();
|
||||
|
||||
@@ -39,11 +39,11 @@ int GetProcessId() {
|
||||
}
|
||||
|
||||
struct BaseConnectionUnix : public BaseConnection {
|
||||
int sock { -1 };
|
||||
int sock{ -1 };
|
||||
};
|
||||
|
||||
static BaseConnectionUnix Connection;
|
||||
static sockaddr_un PipeAddr {};
|
||||
static sockaddr_un PipeAddr{};
|
||||
#ifdef MSG_NOSIGNAL
|
||||
static int MsgFlags = MSG_NOSIGNAL;
|
||||
#else
|
||||
@@ -105,7 +105,7 @@ bool BaseConnection::Open() {
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
12
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
12
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
@@ -38,7 +38,7 @@ int GetProcessId() {
|
||||
}
|
||||
|
||||
struct BaseConnectionWin : public BaseConnection {
|
||||
HANDLE pipe { INVALID_HANDLE_VALUE };
|
||||
HANDLE pipe{ INVALID_HANDLE_VALUE };
|
||||
};
|
||||
|
||||
static BaseConnectionWin Connection;
|
||||
@@ -57,10 +57,10 @@ void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
|
||||
bool BaseConnection::Open() {
|
||||
|
||||
wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" };
|
||||
wchar_t pipeName[]{ L"\\\\?\\pipe\\discord-ipc-0" };
|
||||
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
||||
pipeName[pipeDigit] = L'0';
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
for (;;) {
|
||||
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (self->pipe != INVALID_HANDLE_VALUE) {
|
||||
@@ -88,7 +88,7 @@ bool BaseConnection::Open() {
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
::CloseHandle(self->pipe);
|
||||
self->pipe = INVALID_HANDLE_VALUE;
|
||||
self->isOpen = false;
|
||||
@@ -102,7 +102,7 @@ bool BaseConnection::Write(const void *data, size_t length) {
|
||||
if (length == 0) {
|
||||
return true;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
@@ -127,7 +127,7 @@ bool BaseConnection::Read(void *data, size_t length) {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
|
||||
6
3rdparty/discord-rpc/discord_msg_queue.h
vendored
6
3rdparty/discord-rpc/discord_msg_queue.h
vendored
@@ -34,9 +34,9 @@ namespace discord_rpc {
|
||||
template<typename ElementType, std::size_t QueueSize>
|
||||
class MsgQueue {
|
||||
ElementType queue_[QueueSize];
|
||||
std::atomic_uint nextAdd_ { 0 };
|
||||
std::atomic_uint nextSend_ { 0 };
|
||||
std::atomic_uint pendingSends_ { 0 };
|
||||
std::atomic_uint nextAdd_{ 0 };
|
||||
std::atomic_uint nextSend_{ 0 };
|
||||
std::atomic_uint pendingSends_{ 0 };
|
||||
|
||||
public:
|
||||
MsgQueue() {}
|
||||
|
||||
@@ -163,4 +163,3 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
|
||||
Discord_RegisterW(appId, wcommand);
|
||||
|
||||
}
|
||||
|
||||
|
||||
68
3rdparty/discord-rpc/discord_rpc.cpp
vendored
68
3rdparty/discord-rpc/discord_rpc.cpp
vendored
@@ -40,9 +40,9 @@ static void Discord_UpdateConnection();
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MaxMessageSize { 16 * 1024 };
|
||||
constexpr size_t MessageQueueSize { 8 };
|
||||
constexpr size_t JoinQueueSize { 8 };
|
||||
constexpr size_t MaxMessageSize{ 16 * 1024 };
|
||||
constexpr size_t MessageQueueSize{ 8 };
|
||||
constexpr size_t JoinQueueSize{ 8 };
|
||||
|
||||
struct QueuedMessage {
|
||||
size_t length;
|
||||
@@ -70,24 +70,24 @@ struct User {
|
||||
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
|
||||
};
|
||||
|
||||
static RpcConnection *Connection { nullptr };
|
||||
static DiscordEventHandlers QueuedHandlers {};
|
||||
static DiscordEventHandlers Handlers {};
|
||||
static std::atomic_bool WasJustConnected { false };
|
||||
static std::atomic_bool WasJustDisconnected { false };
|
||||
static std::atomic_bool GotErrorMessage { false };
|
||||
static std::atomic_bool WasJoinGame { false };
|
||||
static std::atomic_bool WasSpectateGame { false };
|
||||
static std::atomic_bool UpdatePresence { false };
|
||||
static RpcConnection *Connection{ nullptr };
|
||||
static DiscordEventHandlers QueuedHandlers{};
|
||||
static DiscordEventHandlers Handlers{};
|
||||
static std::atomic_bool WasJustConnected{ false };
|
||||
static std::atomic_bool WasJustDisconnected{ false };
|
||||
static std::atomic_bool GotErrorMessage{ false };
|
||||
static std::atomic_bool WasJoinGame{ false };
|
||||
static std::atomic_bool WasSpectateGame{ false };
|
||||
static std::atomic_bool UpdatePresence{ false };
|
||||
static char JoinGameSecret[256];
|
||||
static char SpectateGameSecret[256];
|
||||
static int LastErrorCode { 0 };
|
||||
static int LastErrorCode{ 0 };
|
||||
static char LastErrorMessage[256];
|
||||
static int LastDisconnectErrorCode { 0 };
|
||||
static int LastDisconnectErrorCode{ 0 };
|
||||
static char LastDisconnectErrorMessage[256];
|
||||
static std::mutex PresenceMutex;
|
||||
static std::mutex HandlerMutex;
|
||||
static QueuedMessage QueuedPresence {};
|
||||
static QueuedMessage QueuedPresence{};
|
||||
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
||||
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
||||
static User connectedUser;
|
||||
@@ -95,12 +95,12 @@ static User connectedUser;
|
||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
|
||||
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
||||
static auto NextConnect = std::chrono::system_clock::now();
|
||||
static int Pid { 0 };
|
||||
static int Nonce { 1 };
|
||||
static int Pid{ 0 };
|
||||
static int Nonce{ 1 };
|
||||
|
||||
class IoThreadHolder {
|
||||
private:
|
||||
std::atomic_bool keepRunning { true };
|
||||
std::atomic_bool keepRunning{ true };
|
||||
std::mutex waitForIOMutex;
|
||||
std::condition_variable waitForIOActivity;
|
||||
std::thread ioThread;
|
||||
@@ -109,14 +109,14 @@ class IoThreadHolder {
|
||||
void Start() {
|
||||
keepRunning.store(true);
|
||||
ioThread = std::thread([&]() {
|
||||
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
|
||||
Discord_UpdateConnection();
|
||||
while (keepRunning.load()) {
|
||||
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
||||
waitForIOActivity.wait_for(lock, maxWait);
|
||||
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
|
||||
Discord_UpdateConnection();
|
||||
}
|
||||
});
|
||||
while (keepRunning.load()) {
|
||||
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
||||
waitForIOActivity.wait_for(lock, maxWait);
|
||||
Discord_UpdateConnection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Notify() { waitForIOActivity.notify_all(); }
|
||||
@@ -132,7 +132,7 @@ class IoThreadHolder {
|
||||
~IoThreadHolder() { Stop(); }
|
||||
};
|
||||
|
||||
static IoThreadHolder *IoThread { nullptr };
|
||||
static IoThreadHolder *IoThread{ nullptr };
|
||||
|
||||
static void UpdateReconnectTime() {
|
||||
|
||||
@@ -429,7 +429,7 @@ extern "C" void Discord_RunCallbacks() {
|
||||
if (WasJustConnected.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.ready) {
|
||||
DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||
DiscordUser du{ connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||
Handlers.ready(&du);
|
||||
}
|
||||
}
|
||||
@@ -465,7 +465,7 @@ extern "C" void Discord_RunCallbacks() {
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinRequest) {
|
||||
DiscordUser du { req->userId, req->username, req->discriminator, req->avatar };
|
||||
DiscordUser du{ req->userId, req->username, req->discriminator, req->avatar };
|
||||
Handlers.joinRequest(&du);
|
||||
}
|
||||
}
|
||||
@@ -486,12 +486,12 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
||||
|
||||
if (newHandlers) {
|
||||
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||
RegisterForEvent(event); \
|
||||
} \
|
||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
||||
DeregisterForEvent(event); \
|
||||
}
|
||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||
RegisterForEvent(event); \
|
||||
} \
|
||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
||||
DeregisterForEvent(event); \
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
|
||||
|
||||
12
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
12
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
@@ -63,17 +63,17 @@ struct RpcConnection {
|
||||
Connected,
|
||||
};
|
||||
|
||||
BaseConnection *connection { nullptr };
|
||||
State state { State::Disconnected };
|
||||
BaseConnection *connection{ nullptr };
|
||||
State state{ State::Disconnected };
|
||||
void (*onConnect)(JsonDocument &message) { nullptr };
|
||||
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
|
||||
char appId[64] {};
|
||||
int lastErrorCode { 0 };
|
||||
char lastErrorMessage[256] {};
|
||||
char appId[64]{};
|
||||
int lastErrorCode{ 0 };
|
||||
char lastErrorMessage[256]{};
|
||||
RpcConnection::MessageFrame sendFrame;
|
||||
|
||||
static RpcConnection *Create(const char *applicationId);
|
||||
static void Destroy(RpcConnection *&);
|
||||
static void Destroy(RpcConnection*&);
|
||||
|
||||
inline bool IsOpen() const { return state == State::Connected; }
|
||||
|
||||
|
||||
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"
|
||||
|
||||
@@ -6,6 +6,14 @@ if(APPLE)
|
||||
enable_language(OBJC OBJCXX)
|
||||
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)
|
||||
cmake_policy(SET CMP0054 NEW)
|
||||
endif()
|
||||
@@ -32,6 +40,24 @@ if(LINUX)
|
||||
endif()
|
||||
|
||||
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)
|
||||
endif()
|
||||
|
||||
@@ -84,8 +110,6 @@ if(MSVC)
|
||||
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
|
||||
else()
|
||||
list(APPEND COMPILE_OPTIONS
|
||||
$<$<COMPILE_LANGUAGE:C>:-std=c11>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wpedantic
|
||||
@@ -253,11 +277,6 @@ endif()
|
||||
|
||||
find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
|
||||
|
||||
if(APPLE)
|
||||
find_library(SPARKLE Sparkle)
|
||||
#find_package(SPMediaKeyTap REQUIRED)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
find_package(getopt NAMES getopt getopt-win unofficial-getopt-win32 REQUIRED)
|
||||
if(TARGET getopt::getopt)
|
||||
@@ -266,13 +285,15 @@ if(WIN32)
|
||||
set(GETOPT_LIBRARIES getopt-win::getopt)
|
||||
elseif(TARGET getopt::getopt_shared)
|
||||
set(GETOPT_LIBRARIES getopt::getopt_shared)
|
||||
elseif(TARGET unofficial::getopt-win32::getopt)
|
||||
set(GETOPT_LIBRARIES unofficial::getopt-win32::getopt)
|
||||
else()
|
||||
message(FATAL_ERROR "Missing getopt")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(APPLE OR WIN32)
|
||||
find_package(qtsparkle-qt${QT_VERSION_MAJOR})
|
||||
find_package(qtsparkle-qt${QT_VERSION_MAJOR} QUIET)
|
||||
if(TARGET "qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle")
|
||||
set(QTSPARKLE_FOUND ON)
|
||||
endif()
|
||||
@@ -699,6 +720,7 @@ set(SOURCES
|
||||
src/lyrics/elyricsnetlyricsprovider.cpp
|
||||
src/lyrics/letraslyricsprovider.cpp
|
||||
src/lyrics/lyricfindlyricsprovider.cpp
|
||||
src/lyrics/lrcliblyricsprovider.cpp
|
||||
|
||||
src/settings/settingsdialog.cpp
|
||||
src/settings/settingspage.cpp
|
||||
@@ -792,9 +814,7 @@ set(SOURCES
|
||||
src/scrobbler/scrobblercache.cpp
|
||||
src/scrobbler/scrobblercacheitem.cpp
|
||||
src/scrobbler/scrobblemetadata.cpp
|
||||
src/scrobbler/scrobblingapi20.cpp
|
||||
src/scrobbler/lastfmscrobbler.cpp
|
||||
src/scrobbler/librefmscrobbler.cpp
|
||||
src/scrobbler/listenbrainzscrobbler.cpp
|
||||
src/scrobbler/lastfmimport.cpp
|
||||
|
||||
@@ -822,6 +842,8 @@ set(SOURCES
|
||||
|
||||
src/fileview/fileview.cpp
|
||||
src/fileview/fileviewlist.cpp
|
||||
src/fileview/fileviewtree.cpp
|
||||
src/fileview/fileviewtreemodel.cpp
|
||||
|
||||
src/device/devicemanager.cpp
|
||||
src/device/devicelister.cpp
|
||||
@@ -997,6 +1019,7 @@ set(HEADERS
|
||||
src/lyrics/elyricsnetlyricsprovider.h
|
||||
src/lyrics/letraslyricsprovider.h
|
||||
src/lyrics/lyricfindlyricsprovider.h
|
||||
src/lyrics/lrcliblyricsprovider.h
|
||||
|
||||
src/settings/settingsdialog.h
|
||||
src/settings/settingspage.h
|
||||
@@ -1085,9 +1108,7 @@ set(HEADERS
|
||||
src/scrobbler/scrobblersettingsservice.h
|
||||
src/scrobbler/scrobblerservice.h
|
||||
src/scrobbler/scrobblercache.h
|
||||
src/scrobbler/scrobblingapi20.h
|
||||
src/scrobbler/lastfmscrobbler.h
|
||||
src/scrobbler/librefmscrobbler.h
|
||||
src/scrobbler/listenbrainzscrobbler.h
|
||||
src/scrobbler/lastfmimport.h
|
||||
|
||||
@@ -1112,6 +1133,8 @@ set(HEADERS
|
||||
|
||||
src/fileview/fileview.h
|
||||
src/fileview/fileviewlist.h
|
||||
src/fileview/fileviewtree.h
|
||||
src/fileview/fileviewtreemodel.h
|
||||
|
||||
src/device/devicemanager.h
|
||||
src/device/devicelister.h
|
||||
@@ -1214,6 +1237,10 @@ set(UI
|
||||
src/device/deviceviewcontainer.ui
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
optional_source(UNIX SOURCES src/core/unixsignalwatcher.cpp HEADERS src/core/unixsignalwatcher.h)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
optional_source(APPLE
|
||||
SOURCES
|
||||
@@ -1442,6 +1469,7 @@ optional_source(HAVE_SPOTIFY
|
||||
src/spotify/spotifybaserequest.cpp
|
||||
src/spotify/spotifyrequest.cpp
|
||||
src/spotify/spotifyfavoriterequest.cpp
|
||||
src/spotify/spotifymetadatarequest.cpp
|
||||
src/settings/spotifysettingspage.cpp
|
||||
src/covermanager/spotifycoverprovider.cpp
|
||||
HEADERS
|
||||
@@ -1449,6 +1477,7 @@ optional_source(HAVE_SPOTIFY
|
||||
src/spotify/spotifybaserequest.h
|
||||
src/spotify/spotifyrequest.h
|
||||
src/spotify/spotifyfavoriterequest.h
|
||||
src/spotify/spotifymetadatarequest.h
|
||||
src/settings/spotifysettingspage.h
|
||||
src/covermanager/spotifycoverprovider.h
|
||||
UI
|
||||
@@ -1463,6 +1492,8 @@ optional_source(HAVE_QOBUZ
|
||||
src/qobuz/qobuzrequest.cpp
|
||||
src/qobuz/qobuzstreamurlrequest.cpp
|
||||
src/qobuz/qobuzfavoriterequest.cpp
|
||||
src/qobuz/qobuzmetadatarequest.cpp
|
||||
src/qobuz/qobuzcredentialfetcher.cpp
|
||||
src/settings/qobuzsettingspage.cpp
|
||||
src/covermanager/qobuzcoverprovider.cpp
|
||||
HEADERS
|
||||
@@ -1472,6 +1503,8 @@ optional_source(HAVE_QOBUZ
|
||||
src/qobuz/qobuzrequest.h
|
||||
src/qobuz/qobuzstreamurlrequest.h
|
||||
src/qobuz/qobuzfavoriterequest.h
|
||||
src/qobuz/qobuzmetadatarequest.h
|
||||
src/qobuz/qobuzcredentialfetcher.h
|
||||
src/settings/qobuzsettingspage.h
|
||||
src/covermanager/qobuzcoverprovider.h
|
||||
UI
|
||||
@@ -1484,6 +1517,21 @@ qt_add_resources(SOURCES data/data.qrc data/icons.qrc)
|
||||
|
||||
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)
|
||||
|
||||
if(WIN32)
|
||||
@@ -1507,10 +1555,22 @@ if(HAVE_DISCORD_RPC)
|
||||
endif()
|
||||
|
||||
if(HAVE_TRANSLATIONS)
|
||||
option(TRANSLATIONS_VERBOSE "Show verbose output while generating .qm translation files" OFF)
|
||||
# On non-Windows platforms Qt doesn't need a PATH-setup wrapper for tools, but we can
|
||||
# provide a wrapper to filter non-actionable lrelease noise during normal builds.
|
||||
if(NOT CMAKE_HOST_WIN32)
|
||||
set(QT_TOOL_COMMAND_WRAPPER_PATH "${CMAKE_SOURCE_DIR}/cmake/qt_tool_wrapper.sh"
|
||||
CACHE INTERNAL "Wrapper used when invoking Qt tools from CMake" FORCE
|
||||
)
|
||||
endif()
|
||||
qt_add_lupdate(strawberry_lib TS_FILES "${CMAKE_SOURCE_DIR}/src/translations/strawberry_en_US.ts" OPTIONS -locations none -no-ui-lines -no-obsolete)
|
||||
file(GLOB_RECURSE ts_files ${CMAKE_SOURCE_DIR}/src/translations/*.ts)
|
||||
set_source_files_properties(${ts_files} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/data")
|
||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES)
|
||||
if(TRANSLATIONS_VERBOSE)
|
||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES)
|
||||
else()
|
||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES OPTIONS -silent)
|
||||
endif()
|
||||
if(NOT INSTALL_TRANSLATIONS)
|
||||
qt_add_resources(strawberry "translations" PREFIX "/i18n" BASE "${CMAKE_CURRENT_BINARY_DIR}/data" FILES "${INSTALL_TRANSLATIONS_FILES}")
|
||||
endif()
|
||||
|
||||
79
Changelog
79
Changelog
@@ -2,6 +2,83 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
Version 1.2.17 (2026.01.18):
|
||||
|
||||
* Avoid re-scan of restored songs unless mtime is changed (#1819)
|
||||
* Skip existing files when organizing if not overwriting (#1484)
|
||||
* Fixed cursor highlight disappearing off-screen when using down cursor (#1489)
|
||||
* Fixed CD playback only working for the first optical drive (#1852)
|
||||
* Fixed possible race-condition when switching tracks (#1863)
|
||||
* Fixed possible file descriptor exhaustion by using shared thread pool (#1687)
|
||||
* Don't automatically sort playlist with the auto sort option before it's fully loaded (#1690)
|
||||
* Fixed network features stop working after computer suspends and resumes (#1521)
|
||||
* Fixed crash on exit after Qobuz login
|
||||
* Added tag editor option to select ID3v2 version (#1861)
|
||||
* Fixed Qobuz authentication and added automatic credential fetching (#1898)
|
||||
* Fixed playback stopping after deleting a song from disk via context menu (#1783)
|
||||
* Added option to restore smart playlists to the defaults (#1848)
|
||||
* Fixed possible race condition in pipeline destructor (#1875)
|
||||
* Fixed buffering issue near track end during gapless playback (#1725)
|
||||
* Fixed duplicate collection entries for the same artist if they have different sort tags (#1899)
|
||||
* Defer playcount and rating tag writes for currently playing Ogg songs to prevent playback shutter (#1816)
|
||||
* Fixed tag editing not working for Opus sort tags (#1929)
|
||||
* Show playlist load errors (#1470)
|
||||
* Fallback to delete if moving to trash fails (#1679)
|
||||
* Prefer filenames with "front" or "cover" in the filename for album cover art for songs outside of the collection (#1745)
|
||||
* Fixed collection enter/return behavior to respect double-click settings (#1691)
|
||||
* Added tree view mode to files tab (#1922)
|
||||
* Include .webp in allowed extensions for album covers (#1941)
|
||||
* Exit gracefully on SIGTERM signal for Unix systems (#1942)
|
||||
* Optimize the collection scanning process by deferring media file validation from the initial directory scan (#1954)
|
||||
* Fixed collection scan not finding new directories in the top level collection directory when the mountpoint is restored (#1914)
|
||||
* Added genre metadata parsing for Tidal, Qobuz and Spotify (#1913)
|
||||
* Allow editing metadata for stream songs (#1913)
|
||||
* Optimized collection/playlist filtering
|
||||
* Added sort tags to collection/playlist filtering (#1966)
|
||||
|
||||
Version 1.2.16 (2025.12.16):
|
||||
|
||||
* Make Discord Rich presence use filename if song title is missing
|
||||
* Added better error message when a GStreamer plugin is missing
|
||||
* Preserve track order in album shuffle mode when restarting playback (#1623)
|
||||
* Possible fixes for context word wrap
|
||||
* Added lyrics from lrclib.net
|
||||
* Added option to turn off the use of sort tags for the collection
|
||||
* Fixed Spotify login
|
||||
* Fixed error dialog shown minimized if another Strawberry window than the mainwindow was active
|
||||
* Fixed seeking to the end of the track and back causing seeking to stop working (#1675)
|
||||
* Set current index when automatically selecting track (#1825)
|
||||
* Make icon size for shuffle and repeat buttons adjust to screen resolution (#1838)
|
||||
* Fixed song being removed from playlist when dragging to another application (#1815)
|
||||
* Don't automatically scroll on dynamic playlists (#1427)
|
||||
|
||||
Version 1.2.15 (2025.11.25):
|
||||
|
||||
* Fixed system default language not respected
|
||||
* Fixed length filter search
|
||||
* Fixed playlist parser converting Spotify URL's
|
||||
* Removed use of deprecated QStyle::State_Editing
|
||||
* Ignore connection closed errors for ListenBrainz
|
||||
* (Windows) Support building with vcpkg unofficial::getopt-win32::getopt
|
||||
|
||||
Version 1.2.14 (2025.10.25):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed showing error dialog minimized when main window is not current active window (#1739)
|
||||
* Fixed Discord timestamp update when seeking (#1813)
|
||||
* Fixed CD metadata lookup to respect MusicBrainz rate limiting
|
||||
* Fixed Tidal Open API cover provider
|
||||
* (Windows) Fixed device selection with WASAPI2
|
||||
|
||||
Enhancements/Other:
|
||||
* Removed libre.fm support
|
||||
* Rewrote MusicBrainzClient to use Json instead of XML
|
||||
* Subsonic will now use cover art from album when available
|
||||
* Added option to remove "Remastered", etc from song titles for Tidal, Qobuz and Spotify
|
||||
* Added webm to supported file extensions
|
||||
* (Windows|MinGW) Added WASAPI2 support
|
||||
* (Windows) Added experimental exclusive mode for WASAPI2
|
||||
|
||||
Version 1.2.13 (2025.08.31):
|
||||
|
||||
Bugfixes:
|
||||
@@ -266,7 +343,7 @@ Version 1.1.0 (2024.07.14):
|
||||
* Only use playbin3 with GStreamer 1.24 and higher, not with GStreamer 1.22 or lower.
|
||||
* (macOS/Windows) Fixed dash and hls streaming, plugins were missing.
|
||||
* (Windows) Fixed incorrect colors in smart playlist wizard with Fusion in dark mode (#1399).
|
||||
* (Windows) Fixed update window blocking sponsor window on startup.
|
||||
* (Windows) Fixed update window blocking startup window on launch.
|
||||
|
||||
Enhancements:
|
||||
* Improve error messages when connecting and copying to devices.
|
||||
|
||||
51
Formula/kdsingleapplication-qt6.rb
Normal file
51
Formula/kdsingleapplication-qt6.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
class KdsingleapplicationQt6 < Formula
|
||||
desc "Helper class for single-instance Qt applications (Qt 6 build)"
|
||||
homepage "https://github.com/KDAB/KDSingleApplication"
|
||||
url "https://github.com/KDAB/KDSingleApplication/archive/refs/tags/v1.1.0.tar.gz"
|
||||
sha256 "1f19124c0aa5c6fffee3da174f7d2e091fab6dca1e123da70bb0fe615bfbe3e8"
|
||||
license "MIT"
|
||||
|
||||
depends_on "cmake" => :build
|
||||
depends_on "ninja" => :build
|
||||
depends_on "qt"
|
||||
|
||||
def install
|
||||
args = std_cmake_args + %W[
|
||||
-GNinja
|
||||
-DKDSingleApplication_QT6=ON
|
||||
-DKDSingleApplication_TESTS=OFF
|
||||
-DKDSingleApplication_EXAMPLES=OFF
|
||||
-DKDSingleApplication_DOCS=OFF
|
||||
-DKDSingleApplication_DEVELOPER_MODE=OFF
|
||||
-DKDSingleApplication_STATIC=OFF
|
||||
]
|
||||
|
||||
system "cmake", "-S", ".", "-B", "build", *args
|
||||
system "cmake", "--build", "build"
|
||||
system "cmake", "--install", "build"
|
||||
end
|
||||
|
||||
test do
|
||||
# Verify CMake package is usable via find_package(KDSingleApplication-qt6 CONFIG REQUIRED)
|
||||
(testpath/"CMakeLists.txt").write <<~CMAKE
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(kdsa_test LANGUAGES CXX)
|
||||
find_package(KDSingleApplication-qt6 CONFIG REQUIRED)
|
||||
add_executable(test_kdsa main.cpp)
|
||||
target_link_libraries(test_kdsa PRIVATE KDAB::kdsingleapplication)
|
||||
CMAKE
|
||||
|
||||
(testpath/"main.cpp").write <<~CPP
|
||||
#include <QCoreApplication>
|
||||
int main(int argc, char** argv) {
|
||||
QCoreApplication app(argc, argv);
|
||||
return 0;
|
||||
}
|
||||
CPP
|
||||
|
||||
system "cmake", "-S", ".", "-B", "build",
|
||||
"-DCMAKE_PREFIX_PATH=#{opt_prefix}"
|
||||
system "cmake", "--build", "build"
|
||||
end
|
||||
end
|
||||
|
||||
25
Formula/kdsingleapplication-qt6/README.md
Normal file
25
Formula/kdsingleapplication-qt6/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# kdsingleapplication-qt6 (local Homebrew formula)
|
||||
|
||||
This directory exists to keep any supporting files for the local Homebrew formula
|
||||
next to it (e.g. patches or notes).
|
||||
|
||||
## Install (from this Strawberry repo)
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
brew tap strawberry/local "file://$PWD"
|
||||
brew install strawberry/local/kdsingleapplication-qt6
|
||||
```
|
||||
|
||||
## Why it exists
|
||||
|
||||
Strawberry’s build requires the CMake package `KDSingleApplication-qt6`, but it is
|
||||
not consistently available via Homebrew core. Shipping a local formula makes the
|
||||
dependency easy to install for anyone building this repo on macOS.
|
||||
|
||||
## Note for local development
|
||||
|
||||
Homebrew taps are Git clones, so the formula must be committed (or pushed to a remote)
|
||||
to be visible to `brew tap`.
|
||||
|
||||
81
Formula/libgpod.rb
Normal file
81
Formula/libgpod.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
class Libgpod < Formula
|
||||
desc "Library to access the contents of classic iPods"
|
||||
homepage "https://gtkpod.org/libgpod/"
|
||||
url "https://github.com/neuschaefer/libgpod/archive/0dda196286f5e42be89f0b870abd9278213989a5.tar.gz"
|
||||
sha256 "a9809f85b2b763196ac7c94903211a927efd37a24ef39c355c21b4a1bed28e52"
|
||||
license "LGPL-2.0-only"
|
||||
|
||||
depends_on "autoconf" => :build
|
||||
depends_on "automake" => :build
|
||||
depends_on "libtool" => :build
|
||||
depends_on "pkg-config" => :build
|
||||
depends_on "gtk-doc" => :build
|
||||
depends_on "intltool" => :build
|
||||
|
||||
depends_on "glib"
|
||||
depends_on "gdk-pixbuf"
|
||||
depends_on "libplist"
|
||||
depends_on "libxml2"
|
||||
depends_on "sqlite"
|
||||
|
||||
def install
|
||||
# libgpod's configure.ac checks for pkg-config module name "libplist".
|
||||
# Homebrew provides "libplist-2.0", so we provide a tiny shim .pc file to
|
||||
# satisfy the expected name.
|
||||
(buildpath/"brew-pkgconfig").mkpath
|
||||
(buildpath/"brew-pkgconfig/libplist.pc").write <<~EOS
|
||||
prefix=#{Formula["libplist"].opt_prefix}
|
||||
exec_prefix=${prefix}
|
||||
libdir=#{Formula["libplist"].opt_lib}
|
||||
includedir=#{Formula["libplist"].opt_include}
|
||||
|
||||
Name: libplist
|
||||
Description: Apple property list library (Homebrew shim for libgpod)
|
||||
Version: #{Formula["libplist"].version}
|
||||
Libs: -L${libdir} -lplist-2.0
|
||||
Cflags: -I${includedir}
|
||||
EOS
|
||||
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", buildpath/"brew-pkgconfig"
|
||||
|
||||
# Ensure pkg-config can find Homebrew keg .pc files during configure.
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["libplist"].opt_lib/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["sqlite"].opt_lib/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["glib"].opt_lib/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["glib"].opt_share/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["gdk-pixbuf"].opt_lib/"pkgconfig"
|
||||
|
||||
# Upstream's autogen.sh is very old and may hardcode ancient automake checks
|
||||
# (e.g. looking for automake-1.7). Using autoreconf is the standard Homebrew
|
||||
# way and works with modern autotools.
|
||||
#
|
||||
# libgpod's build system expects gtk-doc's makefile snippet to exist (gtk-doc.make),
|
||||
# which is normally provided by running gtkdocize.
|
||||
system "gtkdocize", "--copy"
|
||||
|
||||
# libgpod also uses intltool's Autoconf macros (IT_PROG_INTLTOOL). If intltoolize
|
||||
# is not run, the generated ./configure may contain unexpanded macros and fail.
|
||||
system "intltoolize", "--force", "--copy", "--automake"
|
||||
system "autoreconf", "-fiv"
|
||||
|
||||
system "./configure", *std_configure_args,
|
||||
"--disable-dependency-tracking",
|
||||
"--with-hal=no",
|
||||
"--disable-udev",
|
||||
"--without-libimobiledevice",
|
||||
"--with-python=no",
|
||||
"--with-mono=no",
|
||||
"--disable-gtk-doc",
|
||||
"--disable-gtk-doc-html",
|
||||
"--disable-gtk-doc-pdf",
|
||||
"--enable-more-warnings=no"
|
||||
|
||||
system "make", "install"
|
||||
end
|
||||
|
||||
test do
|
||||
# Ensure pkg-config can find the expected module name used by Strawberry.
|
||||
assert_match "libgpod", shell_output("pkg-config --libs libgpod-1.0")
|
||||
end
|
||||
end
|
||||
|
||||
8
Formula/libgpod/README.md
Normal file
8
Formula/libgpod/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# libgpod (local Homebrew formula)
|
||||
|
||||
Homebrew core does not currently ship `libgpod`, but Strawberry can optionally use it
|
||||
to support **classic iPod** devices (via `libgpod-1.0` + `gdk-pixbuf-2.0`).
|
||||
|
||||
This formula is pinned to a known-good upstream snapshot and disables Linux-specific
|
||||
integration (udev/HAL) and language bindings to keep the build reliable on macOS.
|
||||
|
||||
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)
|
||||
=======================
|
||||
[](https://github.com/sponsors/jonaski)
|
||||
[](https://patreon.com/jonaskvinge)
|
||||
[](https://paypal.me/jonaskvinge)
|
||||
# Strawberry (macOS-focused fork)
|
||||
|
||||
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt framework.
|
||||
This repository is a **macOS-focused fork** of 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/
|
||||
* Wiki: https://wiki.strawberrymusicplayer.org/
|
||||
* Forum: https://forum.strawberrymusicplayer.org/
|
||||
* Github: https://github.com/strawberrymusicplayer/strawberry
|
||||
* Latest builds: https://builds.strawberrymusicplayer.org/
|
||||
* openSUSE buildservice: https://build.opensuse.org/package/show/home:jonaski:audio/strawberry
|
||||
* Ubuntu PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||
* Ubuntu Unstable PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
|
||||
* Translations: https://crowdin.com/project/strawberrymusicplayer/
|
||||
## Upstream vs this fork (macOS distribution)
|
||||
|
||||
### :bangbang: Opening an issue
|
||||
Upstream Strawberry is where ongoing development happens:
|
||||
|
||||
* Read the FAQ: https://wiki.strawberrymusicplayer.org/wiki/FAQ
|
||||
* Search for the issue to see if it is already solved, or if there is an open issue for it already. If there is an open issue already, you can comment on it if you have additional information that could be useful to us.
|
||||
* For technical problems, discussion, questions and feature suggestions use the forum (https://forum.strawberrymusicplayer.org/) instead. The forum is better suited for discussion.
|
||||
* We do not take feature requests from users on GitHub. Any issues related to feature requests will be closed. This does not necessarily mean that we won't add new features, but we don't have time to take feature requests or answer questions about new features from users. It is still possible to suggest or discuss new features on the forum (https://forum.strawberrymusicplayer.org/).
|
||||
* We do not maintain the Flatpak package. Do not report issues related to Flatpak unless the issue can be reproduced with a native package, use Flatpak support instead https://flatpak.org/about/
|
||||
- Upstream: `https://github.com/strawberrymusicplayer/strawberry`
|
||||
|
||||
### :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.
|
||||
There are currently 4 options for sponsoring:
|
||||
- Fork: `https://gitea.dryark.com/dryark/strawberry`
|
||||
|
||||
1. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||
2. [GitHub](https://github.com/sponsors/jonaski)
|
||||
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
||||
4. [PayPal](https://paypal.me/jonaskvinge)
|
||||
This fork is intended for people who want to:
|
||||
|
||||
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
|
||||
* 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/), [elyrics.net](https://www.elyrics.net/), [letras.mus.br](https://www.letras.mus.br) and [LyricFind](https://lyrics.lyricfind.com)
|
||||
* 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
|
||||
## Quick start (macOS)
|
||||
|
||||
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/)
|
||||
* C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/) or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
|
||||
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
||||
* [Boost](https://www.boost.org/)
|
||||
* [GLib](https://developer.gnome.org/glib/)
|
||||
* [Qt 6.4.0 or higher with components Core, Concurrent, Gui, Widgets, Network, Sql and D-Bus](https://www.qt.io/)
|
||||
* [SQLite 3.9 or newer](https://www.sqlite.org)
|
||||
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
|
||||
* [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
* [TagLib 1.12 or higher](https://www.taglib.org/)
|
||||
* [ICU](https://unicode-org.github.io/icu/)
|
||||
* [KDSingleApplication 1.1.0 or higher](https://github.com/KDAB/KDSingleApplication)
|
||||
```bash
|
||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy --dmg \
|
||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
||||
--notary-profile "<profile-name>"
|
||||
```
|
||||
|
||||
Optional dependencies:
|
||||
## Features
|
||||
|
||||
* Song fingerprinting and MusicBrainz tagging: [Chromaprint](https://acoustid.org/chromaprint)
|
||||
* Moodbar: [fftw3](http://www.fftw.org/)
|
||||
* PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/?)
|
||||
* Audio CD: [libcdio](https://www.gnu.org/software/libcdio/)
|
||||
* MTP devices: [libmtp](http://libmtp.sourceforge.net/)
|
||||
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
|
||||
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
|
||||
* Discord rich presence [RapidJSON](https://rapidjson.org/)
|
||||
- Play and organize your music collection
|
||||
- Supports formats: WAV, FLAC, WavPack, Ogg Vorbis, Opus, MPC, TrueAudio, AIFF, MP4, MP3, ASF, and Monkey’s Audio
|
||||
- Audio CD playback
|
||||
- Bit-perfect playback on Linux
|
||||
- Native desktop notifications
|
||||
- Advanced playlist management
|
||||
- Smart and dynamic playlists
|
||||
- Loudness analysis and EBU R128 normalization
|
||||
- Editing tags and fetching missing tags via [MusicBrainz](https://musicbrainz.org/)
|
||||
- Album art from: [Last.fm](https://www.last.fm/), [MusicBrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/), [Spotify](https://www.spotify.com/)
|
||||
- Lyrics from: [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics](https://www.lololyrics.com/), [songlyrics](https://www.songlyrics.com/), [azlyrics](https://www.azlyrics.com/), [elyrics](https://www.elyrics.net/), [letras](https://www.letras.mus.br), [LyricFind](https://lyrics.lyricfind.com) and [lrclib.net](https://lrclib.net/)
|
||||
- Audio analyzer and equalizer
|
||||
- Transfer music to USB, MTP and iPod devices
|
||||
- Scrobbling to [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
||||
- Streaming from Subsonic-compatible servers
|
||||
- Unofficial integrations: Tidal, Spotify, and Qobuz
|
||||
- Discord Rich Presence
|
||||
|
||||
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
|
||||
cmake -S . -B build
|
||||
cmake --build build --parallel $(nproc)
|
||||
sudo cmake --install build
|
||||
|
||||
To build on Windows with Visual Studio 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
For building on Windows with Visual Studio 2022, see: :point_right: https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
|
||||
### :penguin: Packaging status
|
||||
---
|
||||
|
||||
## :package: Packaging status
|
||||
|
||||
[](https://repology.org/metapackage/strawberry/versions)
|
||||
|
||||
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)
|
||||
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing macdeployqt executable.")
|
||||
endif()
|
||||
|
||||
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin)
|
||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||
message(STATUS "Found macdeploycheck: ${MACDEPLOYCHECK_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing macdeploycheck executable.")
|
||||
message(STATUS "macdeploycheck not found (optional): 'deploycheck' target will be unavailable.")
|
||||
endif()
|
||||
|
||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg REQUIRED)
|
||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg)
|
||||
if(CREATEDMG_EXECUTABLE)
|
||||
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing create-dmg executable.")
|
||||
endif()
|
||||
|
||||
set(_SPARKLE_FRAMEWORK_DIR "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "")
|
||||
if(SPARKLE)
|
||||
# SPARKLE may be either the framework directory or the framework binary path.
|
||||
get_filename_component(_sparkle_link "${SPARKLE}" ABSOLUTE)
|
||||
get_filename_component(_sparkle_real "${SPARKLE}" REALPATH)
|
||||
if(_sparkle_link MATCHES "Sparkle\\.framework$")
|
||||
set(_SPARKLE_FRAMEWORK_DIR "${_sparkle_real}")
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "${_sparkle_link}/Versions/B/Sparkle")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "${_sparkle_real}/Versions/B/Sparkle")
|
||||
else()
|
||||
# Assume it's the framework binary path:
|
||||
# .../Sparkle.framework/Versions/B/Sparkle
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "${_sparkle_link}")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "${_sparkle_real}")
|
||||
get_filename_component(_sparkle_b_dir "${_SPARKLE_ORIGINAL_BIN_REAL}" DIRECTORY) # .../Versions/B
|
||||
get_filename_component(_sparkle_versions_dir "${_sparkle_b_dir}" DIRECTORY) # .../Versions
|
||||
get_filename_component(_SPARKLE_FRAMEWORK_DIR "${_sparkle_versions_dir}" DIRECTORY) # .../Sparkle.framework
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${_SPARKLE_FRAMEWORK_DIR}" OR NOT EXISTS "${_SPARKLE_ORIGINAL_BIN_REAL}")
|
||||
set(_SPARKLE_FRAMEWORK_DIR "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "")
|
||||
else()
|
||||
message(STATUS "Sparkle.framework found: ${_SPARKLE_FRAMEWORK_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MACDEPLOYQT_EXECUTABLE)
|
||||
|
||||
if(APPLE_DEVELOPER_ID)
|
||||
set(MACDEPLOYQT_CODESIGN -codesign=${APPLE_DEVELOPER_ID})
|
||||
set(CREATEDMG_CODESIGN --codesign ${APPLE_DEVELOPER_ID})
|
||||
endif()
|
||||
# Note: We intentionally do NOT codesign during the CMake 'deploy'/'dmg' targets.
|
||||
# macdeployqt can optionally sign, but passing identities safely through Ninja's /bin/sh wrapper is brittle.
|
||||
# This repo's signing/notarization pipeline is handled in build_tools/macos/build_sign_notarize.sh instead.
|
||||
if(CREATEDMG_SKIP_JENKINS)
|
||||
set(CREATEDMG_SKIP_JENKINS_ARG "--skip-jenkins")
|
||||
endif()
|
||||
|
||||
add_custom_target(deploy
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/{Frameworks,Resources}
|
||||
set(_deploy_commands
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Frameworks
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources
|
||||
COMMAND cp -v ${CMAKE_BINARY_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
|
||||
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS strawberry
|
||||
)
|
||||
|
||||
if(_SPARKLE_FRAMEWORK_DIR)
|
||||
list(APPEND _deploy_commands
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/bundle_sparkle.sh ${CMAKE_BINARY_DIR}/strawberry.app ${_SPARKLE_FRAMEWORK_DIR} ${_SPARKLE_ORIGINAL_BIN_LINK} ${_SPARKLE_ORIGINAL_BIN_REAL}
|
||||
)
|
||||
endif()
|
||||
|
||||
list(APPEND _deploy_commands
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner
|
||||
)
|
||||
|
||||
# Make 'deploy' incremental:
|
||||
# - add_custom_target() is always out-of-date, so it reruns every time.
|
||||
# - using a stamp file makes Ninja/Make skip deploy when inputs haven't changed.
|
||||
set(_deploy_stamp "${CMAKE_BINARY_DIR}/deploy_app_bundle.stamp")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_deploy_stamp}"
|
||||
${_deploy_commands}
|
||||
COMMAND ${CMAKE_COMMAND} -E touch "${_deploy_stamp}"
|
||||
COMMENT "Deploying app bundle (bundling Sparkle/GStreamer + macdeployqt)"
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS
|
||||
strawberry
|
||||
"${CMAKE_BINARY_DIR}/dist/macos/Info.plist"
|
||||
"${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns"
|
||||
"${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh"
|
||||
"${CMAKE_SOURCE_DIR}/dist/macos/bundle_sparkle.sh"
|
||||
)
|
||||
|
||||
add_custom_target(deploy DEPENDS "${_deploy_stamp}")
|
||||
|
||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||
add_custom_target(deploycheck
|
||||
COMMAND ${MACDEPLOYCHECK_EXECUTABLE} strawberry.app
|
||||
@@ -45,8 +106,9 @@ if(MACDEPLOYQT_EXECUTABLE)
|
||||
endif()
|
||||
if(CREATEDMG_EXECUTABLE)
|
||||
add_custom_target(dmg
|
||||
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_CODESIGN} ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
||||
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS deploy
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
31
cmake/FindRapidJSON.cmake
Normal file
31
cmake/FindRapidJSON.cmake
Normal file
@@ -0,0 +1,31 @@
|
||||
# Try to find RapidJSON (header-only).
|
||||
#
|
||||
# This project uses `find_package(RapidJSON)` and expects:
|
||||
# - RapidJSON_FOUND
|
||||
# - RapidJSON_INCLUDE_DIRS
|
||||
#
|
||||
# Homebrew's `rapidjson` formula commonly installs headers to:
|
||||
# /opt/homebrew/include/rapidjson
|
||||
# but does not always ship a `RapidJSONConfig.cmake`, so we provide this
|
||||
# Find-module fallback.
|
||||
|
||||
find_path(RapidJSON_INCLUDE_DIR
|
||||
NAMES rapidjson/document.h
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(RapidJSON
|
||||
REQUIRED_VARS RapidJSON_INCLUDE_DIR
|
||||
)
|
||||
|
||||
if(RapidJSON_FOUND)
|
||||
set(RapidJSON_INCLUDE_DIRS "${RapidJSON_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
if(RapidJSON_FOUND AND NOT TARGET RapidJSON::RapidJSON)
|
||||
add_library(RapidJSON::RapidJSON INTERFACE IMPORTED)
|
||||
set_target_properties(RapidJSON::RapidJSON PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${RapidJSON_INCLUDE_DIR}"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
set(summary_willbuild "")
|
||||
set(summary_willnotbuild "")
|
||||
|
||||
# On some platforms (notably macOS via Homebrew), many "optional" dependencies are
|
||||
# not installed by default. Historically, Strawberry treated missing optional deps
|
||||
# as a hard error when the option defaulted to ON, which makes first-time builds
|
||||
# frustrating.
|
||||
#
|
||||
# This toggle controls that behavior:
|
||||
# - ON => missing optional deps abort the configure (packager/CI-friendly)
|
||||
# - OFF => missing optional deps auto-disable the component (dev-friendly)
|
||||
set(_optional_components_fatal_default ON)
|
||||
if(APPLE)
|
||||
set(_optional_components_fatal_default OFF)
|
||||
endif()
|
||||
option(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL
|
||||
"If ON, missing optional component dependencies are fatal (otherwise components auto-disable)"
|
||||
${_optional_components_fatal_default}
|
||||
)
|
||||
|
||||
macro(optional_component_summary_add name test)
|
||||
if (${test})
|
||||
list(APPEND summary_willbuild ${name})
|
||||
@@ -80,8 +97,13 @@ function(optional_component name default description)
|
||||
set(text "${description} (missing ${deplist_text})")
|
||||
|
||||
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
|
||||
|
||||
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
||||
if(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL)
|
||||
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
||||
else()
|
||||
message(STATUS "${text} - disabling ${option_variable}")
|
||||
set(${option_variable} OFF CACHE BOOL "${description}" FORCE)
|
||||
return()
|
||||
endif()
|
||||
|
||||
else()
|
||||
set(${have_variable} ON PARENT_SCOPE)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 2)
|
||||
set(STRAWBERRY_VERSION_PATCH 13)
|
||||
set(STRAWBERRY_VERSION_PATCH 17)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
set(INCLUDE_GIT_REVISION ON)
|
||||
|
||||
set(majorminorpatch "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}.${STRAWBERRY_VERSION_PATCH}")
|
||||
|
||||
|
||||
25
cmake/qt_tool_wrapper.sh
Executable file
25
cmake/qt_tool_wrapper.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
tool="${1:-}"
|
||||
shift || true
|
||||
|
||||
if [[ -z "$tool" ]]; then
|
||||
echo "qt_tool_wrapper.sh: missing tool argument" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
base="$(basename "$tool")"
|
||||
|
||||
# Qt LinguistTools (lrelease) prints some noisy informational lines to stderr that
|
||||
# are not actionable during normal builds (e.g. "Removed plural forms...").
|
||||
# We filter only those specific messages.
|
||||
if [[ "$base" == "lrelease" ]]; then
|
||||
"$tool" "$@" 2>&1 | sed \
|
||||
-e '/^Removed plural forms as the target language has less forms\.$/d' \
|
||||
-e '/^If this sounds wrong, possibly the target language is not set or recognized\.$/d'
|
||||
exit "${PIPESTATUS[0]}"
|
||||
fi
|
||||
|
||||
exec "$tool" "$@"
|
||||
|
||||
4
debian/control
vendored
4
debian/control
vendored
@@ -60,11 +60,11 @@ Description: music player and music collection organizer
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
||||
- Lyrics from multiple sources
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
- Unofficial streaming support for Tidal and Qobuz
|
||||
.
|
||||
|
||||
22
dist/CMakeLists.txt
vendored
22
dist/CMakeLists.txt
vendored
@@ -9,7 +9,27 @@ if(APPLE)
|
||||
else()
|
||||
set(LSMinimumSystemVersion 12.0)
|
||||
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()
|
||||
|
||||
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>
|
||||
<string>strawberry.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.strawberrymusicplayer.strawberry</string>
|
||||
<string>@MACOS_BUNDLE_ID@</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLongVersionString</key>
|
||||
@@ -34,17 +34,24 @@
|
||||
<string>public.app-category.music</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<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>
|
||||
<string>https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@</string>
|
||||
<string>@SPARKLE_FEED_URL@</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=</string>
|
||||
<string>@SPARKLE_PUBLIC_ED25519_KEY@</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>org.strawberrymusicplayer.strawberry</string>
|
||||
<string>@MACOS_BUNDLE_ID@</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<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>Automatically retrieve tags from MusicBrainz</li>
|
||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind</li>
|
||||
<li>Lyrics from multiple sources</li>
|
||||
<li>Audio analyzer and equalizer</li>
|
||||
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
||||
<li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li>
|
||||
<li>Scrobbler with support for Last.fm and ListenBrainz</li>
|
||||
<li>Streaming support for Subsonic-compatible servers</li>
|
||||
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
||||
</ul>
|
||||
@@ -51,6 +51,10 @@
|
||||
</screenshots>
|
||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||
<releases>
|
||||
<release version="1.2.17" date="2026-01-18"/>
|
||||
<release version="1.2.16" date="2025-12-16"/>
|
||||
<release version="1.2.15" date="2025-11-25"/>
|
||||
<release version="1.2.14" date="2025-10-25"/>
|
||||
<release version="1.2.13" date="2025-08-31"/>
|
||||
<release version="1.2.12" date="2025-08-12"/>
|
||||
<release version="1.2.11" date="2025-05-15"/>
|
||||
|
||||
6
dist/unix/strawberry.1
vendored
6
dist/unix/strawberry.1
vendored
@@ -29,9 +29,7 @@ Features:
|
||||
.br
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
.br
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
||||
.br
|
||||
- Support for multiple backends
|
||||
- Lyrics from multiple sources
|
||||
.br
|
||||
- Audio analyzer
|
||||
.br
|
||||
@@ -39,7 +37,7 @@ Features:
|
||||
.br
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
.br
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
.br
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
.br
|
||||
|
||||
7
dist/unix/strawberry.spec.in
vendored
7
dist/unix/strawberry.spec.in
vendored
@@ -93,16 +93,15 @@ Features:
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
||||
- Support for multiple backends
|
||||
- Lyrics from multiple sources
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||
- Scrobbler with support for Last.fm and ListenBrainz
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
- Streaming support for Subsonic-compatible servers
|
||||
- Unofficial streaming support for Tidal and Qobuz
|
||||
|
||||
%if 0%{?suse_version} && 0%{?suse_version} <= 1600
|
||||
%if 0%{?suse_version} && 0%{?suse_version} < 1600
|
||||
%debug_package
|
||||
%endif
|
||||
|
||||
|
||||
78
dist/windows/strawberry.nsi.in
vendored
78
dist/windows/strawberry.nsi.in
vendored
@@ -395,9 +395,6 @@ Section "Strawberry" Strawberry
|
||||
File "glib-2.0-0.dll"
|
||||
File "gme.dll"
|
||||
File "gmodule-2.0-0.dll"
|
||||
!ifndef arch_arm64
|
||||
File "gnutls.dll"
|
||||
!endif
|
||||
File "gobject-2.0-0.dll"
|
||||
File "gstadaptivedemux-1.0-0.dll"
|
||||
File "gstapp-1.0-0.dll"
|
||||
@@ -418,18 +415,11 @@ Section "Strawberry" Strawberry
|
||||
File "gsttag-1.0-0.dll"
|
||||
File "gsturidownloader-1.0-0.dll"
|
||||
File "gstvideo-1.0-0.dll"
|
||||
!ifdef arch_arm64
|
||||
File "gstwinrt-1.0-0.dll"
|
||||
!endif
|
||||
File "harfbuzz.dll"
|
||||
File "intl-8.dll"
|
||||
File "jpeg62.dll"
|
||||
File "kdsingleapplication-qt6.dll"
|
||||
File "libbs2b.dll"
|
||||
!ifndef arch_arm64
|
||||
File "libfaac_dll.dll"
|
||||
!endif
|
||||
File "liblzma.dll"
|
||||
File "libmp3lame.dll"
|
||||
File "libopenmpt.dll"
|
||||
File "mpcdec.dll"
|
||||
@@ -447,6 +437,11 @@ Section "Strawberry" Strawberry
|
||||
File "vorbisfile.dll"
|
||||
File "wavpackdll.dll"
|
||||
|
||||
!ifndef arch_arm64
|
||||
File "gnutls.dll"
|
||||
File "libfaac_dll.dll"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
File "freetype.dll"
|
||||
File "libiconv.dll"
|
||||
@@ -454,10 +449,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeex.dll"
|
||||
File "pcre2-8.dll"
|
||||
File "pcre2-16.dll"
|
||||
File "zlib1.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolame.dll"
|
||||
!endif
|
||||
File "zlib1.dll"
|
||||
!endif
|
||||
!ifdef debug
|
||||
File "freetyped.dll"
|
||||
@@ -466,10 +461,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeexd.dll"
|
||||
File "pcre2-8d.dll"
|
||||
File "pcre2-16d.dll"
|
||||
File "zlibd1.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolamed.dll"
|
||||
!endif
|
||||
File "zlibd1.dll"
|
||||
!endif
|
||||
|
||||
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
|
||||
@@ -482,15 +477,15 @@ Section "Strawberry" Strawberry
|
||||
|
||||
; Common files
|
||||
|
||||
File "icudt77.dll"
|
||||
File "icudt78.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
File "fftw3.dll"
|
||||
!else
|
||||
File "libfftw3-3.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
File "icuin77d.dll"
|
||||
File "icuuc77d.dll"
|
||||
File "icuin78d.dll"
|
||||
File "icuuc78d.dll"
|
||||
File "libxml2d.dll"
|
||||
File "Qt6Concurrentd.dll"
|
||||
File "Qt6Cored.dll"
|
||||
@@ -499,8 +494,8 @@ Section "Strawberry" Strawberry
|
||||
File "Qt6Sqld.dll"
|
||||
File "Qt6Widgetsd.dll"
|
||||
!else
|
||||
File "icuin77.dll"
|
||||
File "icuuc77.dll"
|
||||
File "icuin78.dll"
|
||||
File "icuuc78.dll"
|
||||
File "libxml2.dll"
|
||||
File "Qt6Concurrent.dll"
|
||||
File "Qt6Core.dll"
|
||||
@@ -510,6 +505,7 @@ Section "Strawberry" Strawberry
|
||||
File "Qt6Widgets.dll"
|
||||
!endif
|
||||
|
||||
!ifdef msvc && arch_x86
|
||||
File "avcodec-61.dll"
|
||||
File "avfilter-10.dll"
|
||||
File "avformat-61.dll"
|
||||
@@ -517,6 +513,14 @@ Section "Strawberry" Strawberry
|
||||
File "postproc-58.dll"
|
||||
File "swresample-5.dll"
|
||||
File "swscale-8.dll"
|
||||
!else
|
||||
File "avcodec-62.dll"
|
||||
File "avfilter-11.dll"
|
||||
File "avformat-62.dll"
|
||||
File "avutil-60.dll"
|
||||
File "swresample-6.dll"
|
||||
File "swscale-9.dll"
|
||||
!endif
|
||||
|
||||
; Register Strawberry with Default Programs
|
||||
Var /GLOBAL AppIcon
|
||||
@@ -677,6 +681,7 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=libgstvolume.dll" "gstreamer-plugins\libgstvolume.dll"
|
||||
File "/oname=libgstvorbis.dll" "gstreamer-plugins\libgstvorbis.dll"
|
||||
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
|
||||
File "/oname=libgstwasapi2.dll" "gstreamer-plugins\libgstwasapi2.dll"
|
||||
File "/oname=libgstwaveform.dll" "gstreamer-plugins\libgstwaveform.dll"
|
||||
File "/oname=libgstwavenc.dll" "gstreamer-plugins\libgstwavenc.dll"
|
||||
File "/oname=libgstwavpack.dll" "gstreamer-plugins\libgstwavpack.dll"
|
||||
@@ -942,9 +947,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\glib-2.0-0.dll"
|
||||
Delete "$INSTDIR\gme.dll"
|
||||
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\gobject-2.0-0.dll"
|
||||
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstapp-1.0-0.dll"
|
||||
@@ -965,18 +967,11 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gsttag-1.0-0.dll"
|
||||
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstvideo-1.0-0.dll"
|
||||
!ifdef arch_arm64
|
||||
Delete "$INSTDIR\gstwinrt-1.0-0.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\harfbuzz.dll"
|
||||
Delete "$INSTDIR\intl-8.dll"
|
||||
Delete "$INSTDIR\jpeg62.dll"
|
||||
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
|
||||
Delete "$INSTDIR\libbs2b.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\liblzma.dll"
|
||||
Delete "$INSTDIR\libmp3lame.dll"
|
||||
Delete "$INSTDIR\libopenmpt.dll"
|
||||
Delete "$INSTDIR\mpcdec.dll"
|
||||
@@ -994,6 +989,11 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\vorbisfile.dll"
|
||||
Delete "$INSTDIR\wavpackdll.dll"
|
||||
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
Delete "$INSTDIR\freetype.dll"
|
||||
Delete "$INSTDIR\libiconv.dll"
|
||||
@@ -1001,10 +1001,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeex.dll"
|
||||
Delete "$INSTDIR\pcre2-8.dll"
|
||||
Delete "$INSTDIR\pcre2-16.dll"
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolame.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
!endif
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\freetyped.dll"
|
||||
@@ -1013,10 +1013,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeexd.dll"
|
||||
Delete "$INSTDIR\pcre2-8d.dll"
|
||||
Delete "$INSTDIR\pcre2-16d.dll"
|
||||
Delete "$INSTDIR\zlibd1.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolamed.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\zlibd1.dll"
|
||||
!endif
|
||||
|
||||
!ifdef arch_x86
|
||||
@@ -1028,15 +1028,15 @@ Section "Uninstall"
|
||||
|
||||
; Common files
|
||||
|
||||
Delete "$INSTDIR\icudt77.dll"
|
||||
Delete "$INSTDIR\icudt78.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
Delete "$INSTDIR\fftw3.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libfftw3-3.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
Delete "$INSTDIR\icuin77d.dll"
|
||||
Delete "$INSTDIR\icuuc77d.dll"
|
||||
Delete "$INSTDIR\icuin78d.dll"
|
||||
Delete "$INSTDIR\icuuc78d.dll"
|
||||
Delete "$INSTDIR\libxml2d.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrentd.dll"
|
||||
Delete "$INSTDIR\Qt6Cored.dll"
|
||||
@@ -1045,8 +1045,8 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\Qt6Sqld.dll"
|
||||
Delete "$INSTDIR\Qt6Widgetsd.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\icuin77.dll"
|
||||
Delete "$INSTDIR\icuuc77.dll"
|
||||
Delete "$INSTDIR\icuin78.dll"
|
||||
Delete "$INSTDIR\icuuc78.dll"
|
||||
Delete "$INSTDIR\libxml2.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrent.dll"
|
||||
Delete "$INSTDIR\Qt6Core.dll"
|
||||
@@ -1056,6 +1056,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\Qt6Widgets.dll"
|
||||
!endif
|
||||
|
||||
!ifdef msvc && arch_x86
|
||||
Delete "$INSTDIR\avcodec-61.dll"
|
||||
Delete "$INSTDIR\avfilter-10.dll"
|
||||
Delete "$INSTDIR\avformat-61.dll"
|
||||
@@ -1063,6 +1064,14 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\postproc-58.dll"
|
||||
Delete "$INSTDIR\swresample-5.dll"
|
||||
Delete "$INSTDIR\swscale-8.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\avcodec-62.dll"
|
||||
Delete "$INSTDIR\avfilter-11.dll"
|
||||
Delete "$INSTDIR\avformat-62.dll"
|
||||
Delete "$INSTDIR\avutil-60.dll"
|
||||
Delete "$INSTDIR\swresample-6.dll"
|
||||
Delete "$INSTDIR\swscale-9.dll"
|
||||
!endif
|
||||
|
||||
!ifdef mingw
|
||||
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
|
||||
@@ -1156,6 +1165,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstvolume.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstvorbis.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi2.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwaveform.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavenc.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwavpack.dll"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef ANALYZERBASE_H
|
||||
#define ANALYZERBASE_H
|
||||
@@ -90,4 +90,3 @@ class AnalyzerBase : public QWidget {
|
||||
};
|
||||
|
||||
#endif // ANALYZERBASE_H
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
@@ -61,7 +61,7 @@ constexpr int kLowFramerate = 20;
|
||||
constexpr int kMediumFramerate = 25;
|
||||
constexpr int kHighFramerate = 30;
|
||||
constexpr int kSuperHighFramerate = 60;
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
AnalyzerContainer::AnalyzerContainer(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef ANALYZERCONTAINER_H
|
||||
#define ANALYZERCONTAINER_H
|
||||
@@ -101,8 +101,7 @@ void AnalyzerContainer::AddAnalyzerType() {
|
||||
group_->addAction(action);
|
||||
action->setCheckable(true);
|
||||
actions_ << action;
|
||||
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); } );
|
||||
|
||||
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); });
|
||||
}
|
||||
|
||||
#endif // ANALYZERCONTAINER_H
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "blockanalyzer.h"
|
||||
|
||||
@@ -61,11 +61,12 @@ BlockAnalyzer::BlockAnalyzer(QWidget *parent)
|
||||
fade_intensity_(1 << 8, 32),
|
||||
step_(0) {
|
||||
|
||||
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1); //-1 is padding, no drawing takes place there
|
||||
setMinimumSize(kMinColumns * (kWidth + 1) - 1, kMinRows * (kHeight + 1) - 1); // -1 is padding, no drawing takes place there
|
||||
setMaximumWidth(kMaxColumns * (kWidth + 1) - 1);
|
||||
|
||||
// mxcl says null pixmaps cause crashes, so let's play it safe
|
||||
std::fill(fade_bars_.begin(), fade_bars_.end(), QPixmap(1, 1));
|
||||
|
||||
}
|
||||
|
||||
void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
|
||||
@@ -237,7 +238,7 @@ static inline void adjustToLimits(const int b, int &f, int &amount) {
|
||||
* Clever contrast function
|
||||
*
|
||||
* It will try to adjust the foreground color such that it contrasts well with
|
||||
*the background
|
||||
* the background
|
||||
* It won't modify the hue of fg unless absolutely necessary
|
||||
* @return the adjusted form of fg
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef BLOCKANALYZER_H
|
||||
#define BLOCKANALYZER_H
|
||||
@@ -41,14 +41,14 @@ class BlockAnalyzer : public AnalyzerBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE explicit BlockAnalyzer(QWidget*);
|
||||
Q_INVOKABLE explicit BlockAnalyzer(QWidget *parent);
|
||||
|
||||
static const char *kName;
|
||||
|
||||
protected:
|
||||
void transform(Scope&) override;
|
||||
void transform(Scope &s) override;
|
||||
void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
|
||||
void resizeEvent(QResizeEvent*) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
virtual void paletteChange(const QPalette &_palette);
|
||||
void framerateChanged() override;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "boomanalyzer.h"
|
||||
|
||||
@@ -143,7 +143,7 @@ void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame
|
||||
bar_height_[i] = std::max(0.0, bar_height_[i]);
|
||||
}
|
||||
|
||||
peak_handling:
|
||||
peak_handling:
|
||||
|
||||
if (peak_height_[i] > 0.0) {
|
||||
peak_height_[i] -= peak_speed_[i];
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef BOOMANALYZER_H
|
||||
#define BOOMANALYZER_H
|
||||
@@ -40,7 +40,7 @@ class BoomAnalyzer : public AnalyzerBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE explicit BoomAnalyzer(QWidget*);
|
||||
Q_INVOKABLE explicit BoomAnalyzer(QWidget *parent);
|
||||
|
||||
static const char *kName;
|
||||
|
||||
@@ -70,7 +70,6 @@ class BoomAnalyzer : public AnalyzerBase {
|
||||
|
||||
QPixmap barPixmap_;
|
||||
QPixmap canvas_;
|
||||
|
||||
};
|
||||
|
||||
#endif // BOOMANALYZER_H
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "fht.h"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef FHT_H
|
||||
#define FHT_H
|
||||
@@ -55,20 +55,20 @@ class FHT {
|
||||
/**
|
||||
* Recursive in-place Hartley transform. For internal use only!
|
||||
*/
|
||||
void _transform(float*, int, int);
|
||||
void _transform(float *p, int n, int k);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Prepare transform for data sets with @f$2^n@f$ numbers, whereby @f$n@f$
|
||||
* should be at least 3. Values of more than 3 need a trigonometry table.
|
||||
* @see makeCasTable()
|
||||
*/
|
||||
* Prepare transform for data sets with @f$2^n@f$ numbers, whereby @f$n@f$
|
||||
* should be at least 3. Values of more than 3 need a trigonometry table.
|
||||
* @see makeCasTable()
|
||||
*/
|
||||
explicit FHT(uint);
|
||||
|
||||
~FHT();
|
||||
int sizeExp() const;
|
||||
int size() const;
|
||||
void scale(float*, float) const;
|
||||
void scale(float *p, float d) const;
|
||||
|
||||
/**
|
||||
* Exponentially Weighted Moving Average (EWMA) filter.
|
||||
@@ -90,12 +90,12 @@ class FHT {
|
||||
/**
|
||||
* Semi-logarithmic audio spectrum.
|
||||
*/
|
||||
void semiLogSpectrum(float*);
|
||||
void semiLogSpectrum(float *p);
|
||||
|
||||
/**
|
||||
* Fourier spectrum.
|
||||
*/
|
||||
void spectrum(float*);
|
||||
void spectrum(float *p);
|
||||
|
||||
/**
|
||||
* Calculates a mathematically correct FFT power spectrum.
|
||||
@@ -103,7 +103,7 @@ class FHT {
|
||||
* and factor the 0.5 in the final scaling factor.
|
||||
* @see FHT::power2()
|
||||
*/
|
||||
void power(float*);
|
||||
void power(float *p);
|
||||
|
||||
/**
|
||||
* Calculates an FFT power spectrum with doubled values as a
|
||||
@@ -112,14 +112,14 @@ class FHT {
|
||||
* of @f$2^n@f$ input values. This is the fastest transform.
|
||||
* @see FHT::power()
|
||||
*/
|
||||
void power2(float*);
|
||||
void power2(float *p);
|
||||
|
||||
/**
|
||||
* Discrete Hartley transform of data sets with 8 values.
|
||||
*/
|
||||
static void transform8(float*);
|
||||
static void transform8(float *p);
|
||||
|
||||
void transform(float*);
|
||||
void transform(float *p);
|
||||
};
|
||||
|
||||
#endif // FHT_H
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "rainbowanalyzer.h"
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef RAINBOWANALYZER_H
|
||||
#define RAINBOWANALYZER_H
|
||||
@@ -85,7 +85,7 @@ class RainbowAnalyzer : public AnalyzerBase {
|
||||
|
||||
private:
|
||||
// "constants" that get initialized in the constructor
|
||||
float band_scale_[kRainbowBands] {};
|
||||
float band_scale_[kRainbowBands]{};
|
||||
QPen colors_[kRainbowBands];
|
||||
|
||||
// Rainbow Nyancat & Dash
|
||||
@@ -96,7 +96,7 @@ class RainbowAnalyzer : public AnalyzerBase {
|
||||
int frame_;
|
||||
|
||||
// The y positions of each point on the rainbow.
|
||||
float history_[kHistorySize * kRainbowBands] {};
|
||||
float history_[kHistorySize * kRainbowBands]{};
|
||||
|
||||
// A cache of the last frame's rainbow,
|
||||
// so it can be used in the next frame.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef SONOGRAMANALYZER_H
|
||||
#define SONOGRAMANALYZER_H
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
@@ -70,7 +70,7 @@ void TurbineAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_fr
|
||||
bar_height_[i] = std::max(0.0, bar_height_[i]);
|
||||
}
|
||||
|
||||
peak_handling:
|
||||
peak_handling:
|
||||
if (peak_height_[i] > 0.0) {
|
||||
peak_height_[i] -= peak_speed_[i];
|
||||
peak_speed_[i] *= F_peakSpeed_; // 1.12
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
#ifndef TURBINEANALYZER_H
|
||||
#define TURBINEANALYZER_H
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
*/
|
||||
|
||||
|
||||
#include <QPixmap>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -537,10 +537,24 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
if (subdir.mtime == 0) {
|
||||
// Delete the subdirectory
|
||||
// See if this subdirectory already exists in the database
|
||||
bool exists = false;
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
exists = q.next();
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
@@ -549,42 +563,36 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
||||
}
|
||||
}
|
||||
else {
|
||||
// See if this subdirectory already exists in the database
|
||||
bool exists = false;
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
exists = q.next();
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
transaction.Commit();
|
||||
|
||||
}
|
||||
|
||||
void CollectionBackend::DeleteSubdirs(const CollectionSubdirectoryList &subdirs) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -140,7 +140,6 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
Q_INVOKABLE explicit CollectionBackend(QObject *parent = nullptr);
|
||||
|
||||
~CollectionBackend();
|
||||
@@ -253,6 +252,7 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
void DeleteSongsByUrls(const QList<QUrl> &url);
|
||||
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
|
||||
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||
void DeleteSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||
void CompilationsNeedUpdating();
|
||||
void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded);
|
||||
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual);
|
||||
@@ -331,4 +331,3 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
};
|
||||
|
||||
#endif // COLLECTIONBACKEND_H
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
class CollectionFilterOptions {
|
||||
public:
|
||||
|
||||
explicit CollectionFilterOptions();
|
||||
|
||||
// Filter mode:
|
||||
|
||||
@@ -135,4 +135,3 @@ class CollectionFilterWidget : public QWidget {
|
||||
};
|
||||
|
||||
#endif // COLLECTIONFILTERWIDGET_H
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -124,6 +124,7 @@ void CollectionLibrary::Init() {
|
||||
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
|
||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsDeleted, &*backend_, &CollectionBackend::DeleteSubdirs);
|
||||
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
|
||||
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);
|
||||
|
||||
@@ -189,6 +190,26 @@ void CollectionLibrary::ReloadSettings() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::CurrentSongChanged(const Song &song) {
|
||||
|
||||
current_song_url_ = song.url();
|
||||
|
||||
if (!pending_song_saves_.isEmpty()) {
|
||||
SavePendingPlaycountsAndRatings();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::Stopped() {
|
||||
|
||||
current_song_url_ = QUrl();
|
||||
|
||||
if (!pending_song_saves_.isEmpty()) {
|
||||
SavePendingPlaycountsAndRatings();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SyncPlaycountAndRatingToFilesAsync() {
|
||||
|
||||
(void)QtConcurrent::run(&CollectionLibrary::SyncPlaycountAndRatingToFiles, this);
|
||||
@@ -212,18 +233,85 @@ void CollectionLibrary::SyncPlaycountAndRatingToFiles() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SongsPlaycountChanged(const SongList &songs, const bool save_tags) const {
|
||||
void CollectionLibrary::SongsPlaycountChanged(const SongList &songs, const bool save_tags) {
|
||||
|
||||
if (save_tags || save_playcounts_to_files_) {
|
||||
tagreader_client_->SaveSongsPlaycountAsync(songs);
|
||||
SongList songs_to_save_now;
|
||||
for (const Song &song : songs) {
|
||||
if (song.url().isLocalFile() && song.url() == current_song_url_ &&
|
||||
(song.filetype() == Song::FileType::OggFlac || song.filetype() == Song::FileType::OggVorbis || song.filetype() == Song::FileType::OggOpus)) {
|
||||
qLog(Debug) << "Deferring playcount save for currently playing file" << song.url().toLocalFile();
|
||||
if (pending_song_saves_.contains(song.url())) {
|
||||
SharedPtr<PendingSongSave> pending_song_save = pending_song_saves_[song.url()];
|
||||
pending_song_save->save_playcount = true;
|
||||
pending_song_save->song.set_playcount(song.playcount());
|
||||
}
|
||||
else {
|
||||
SharedPtr<PendingSongSave> pending_song_save = make_shared<PendingSongSave>();
|
||||
pending_song_save->save_playcount = true;
|
||||
pending_song_save->song = song;
|
||||
pending_song_saves_.insert(song.url(), pending_song_save);
|
||||
}
|
||||
}
|
||||
else {
|
||||
songs_to_save_now << song;
|
||||
}
|
||||
}
|
||||
if (!songs_to_save_now.isEmpty()) {
|
||||
tagreader_client_->SaveSongsPlaycountAsync(songs_to_save_now);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SongsRatingChanged(const SongList &songs, const bool save_tags) const {
|
||||
void CollectionLibrary::SongsRatingChanged(const SongList &songs, const bool save_tags) {
|
||||
|
||||
if (save_tags || save_ratings_to_files_) {
|
||||
tagreader_client_->SaveSongsRatingAsync(songs);
|
||||
SongList songs_to_save_now;
|
||||
for (const Song &song : songs) {
|
||||
if (song.url().isLocalFile() && song.url() == current_song_url_ &&
|
||||
(song.filetype() == Song::FileType::OggFlac || song.filetype() == Song::FileType::OggVorbis || song.filetype() == Song::FileType::OggOpus)) {
|
||||
qLog(Debug) << "Deferring rating save for currently playing file" << song.url().toLocalFile();
|
||||
if (pending_song_saves_.contains(song.url())) {
|
||||
SharedPtr<PendingSongSave> pending_song_save = pending_song_saves_[song.url()];
|
||||
pending_song_save->save_rating = true;
|
||||
pending_song_save->song.set_rating(song.rating());
|
||||
}
|
||||
else {
|
||||
SharedPtr<PendingSongSave> pending_song_save = make_shared<PendingSongSave>();
|
||||
pending_song_save->save_rating = true;
|
||||
pending_song_save->song = song;
|
||||
pending_song_saves_.insert(song.url(), pending_song_save);
|
||||
}
|
||||
}
|
||||
else {
|
||||
songs_to_save_now << song;
|
||||
}
|
||||
}
|
||||
if (!songs_to_save_now.isEmpty()) {
|
||||
tagreader_client_->SaveSongsRatingAsync(songs_to_save_now);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SavePendingPlaycountsAndRatings() {
|
||||
|
||||
for (auto it = pending_song_saves_.constBegin(); it != pending_song_saves_.constEnd();) {
|
||||
const QUrl url = it.key();
|
||||
SharedPtr<PendingSongSave> pending_song_save = it.value();
|
||||
if (url == current_song_url_) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
qLog(Debug) << "Saving deferred playcount/rating for" << url.toLocalFile();
|
||||
if (pending_song_save->save_playcount) {
|
||||
tagreader_client_->SaveSongsPlaycountAsync(SongList() << pending_song_save->song);
|
||||
}
|
||||
if (pending_song_save->save_rating) {
|
||||
tagreader_client_->SaveSongsRatingAsync(SongList() << pending_song_save->song);
|
||||
}
|
||||
it = pending_song_saves_.erase(it);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
@@ -71,6 +72,7 @@ class CollectionLibrary : public QObject {
|
||||
|
||||
private:
|
||||
void SyncPlaycountAndRatingToFiles();
|
||||
void SavePendingPlaycountsAndRatings();
|
||||
|
||||
public Q_SLOTS:
|
||||
void ReloadSettings();
|
||||
@@ -84,16 +86,26 @@ class CollectionLibrary : public QObject {
|
||||
|
||||
void IncrementalScan();
|
||||
|
||||
void CurrentSongChanged(const Song &song);
|
||||
void Stopped();
|
||||
|
||||
private Q_SLOTS:
|
||||
void ExitReceived();
|
||||
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false) const;
|
||||
void SongsRatingChanged(const SongList &songs, const bool save_tags = false) const;
|
||||
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false);
|
||||
void SongsRatingChanged(const SongList &songs, const bool save_tags = false);
|
||||
|
||||
Q_SIGNALS:
|
||||
void Error(const QString &error);
|
||||
void ExitFinished();
|
||||
|
||||
private:
|
||||
class PendingSongSave {
|
||||
public:
|
||||
Song song;
|
||||
bool save_playcount = false;
|
||||
bool save_rating = false;
|
||||
};
|
||||
|
||||
const SharedPtr<TaskManager> task_manager_;
|
||||
const SharedPtr<TagReaderClient> tagreader_client_;
|
||||
|
||||
@@ -111,6 +123,10 @@ class CollectionLibrary : public QObject {
|
||||
|
||||
bool save_playcounts_to_files_;
|
||||
bool save_ratings_to_files_;
|
||||
|
||||
QUrl current_song_url_;
|
||||
|
||||
QMap<QUrl, SharedPtr<PendingSongSave>> pending_song_saves_;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -212,6 +212,7 @@ void CollectionModel::ReloadSettings() {
|
||||
const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool();
|
||||
const bool sort_skip_articles_for_artists = settings.value(CollectionSettings::kSkipArticlesForArtists, true).toBool();
|
||||
const bool sort_skip_articles_for_albums = settings.value(CollectionSettings::kSkipArticlesForAlbums, false).toBool();
|
||||
const bool use_sort_tags = settings.value(CollectionSettings::kUseSortTags, true).toBool();
|
||||
|
||||
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
|
||||
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
|
||||
@@ -227,12 +228,14 @@ void CollectionModel::ReloadSettings() {
|
||||
show_dividers != options_current_.show_dividers ||
|
||||
show_various_artists != options_current_.show_various_artists ||
|
||||
sort_skip_articles_for_artists != options_current_.sort_skip_articles_for_artists ||
|
||||
sort_skip_articles_for_albums != options_current_.sort_skip_articles_for_albums) {
|
||||
sort_skip_articles_for_albums != options_current_.sort_skip_articles_for_albums ||
|
||||
use_sort_tags != options_current_.use_sort_tags) {
|
||||
options_current_.show_pretty_covers = show_pretty_covers;
|
||||
options_current_.show_dividers = show_dividers;
|
||||
options_current_.show_various_artists = show_various_artists;
|
||||
options_current_.sort_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();
|
||||
}
|
||||
|
||||
@@ -427,6 +430,8 @@ 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);
|
||||
|
||||
}
|
||||
@@ -540,7 +545,10 @@ void CollectionModel::AddSongsInternal(const SongList &songs) {
|
||||
// Sanity check to make sure we don't add songs that are outside the user's filter
|
||||
if (!options_active_.filter_options.Matches(song)) continue;
|
||||
|
||||
if (song_nodes_.contains(song.id())) continue;
|
||||
if (song_nodes_.contains(song.id())) {
|
||||
qLog(Debug) << song.id() << song.title() << "already exists, skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Before we can add each song we need to make sure the required container items already exist in the tree.
|
||||
// These depend on which "group by" settings the user has on the collection.
|
||||
@@ -681,8 +689,8 @@ void CollectionModel::RemoveSongsInternal(const SongList &songs) {
|
||||
if (!divider_nodes_.contains(divider_key)) continue;
|
||||
|
||||
// Look to see if there are any other items still under this divider
|
||||
QList<CollectionItem*> container_nodes = container_nodes_[0].values();
|
||||
if (std::any_of(container_nodes.begin(), container_nodes.end(), [this, divider_key](CollectionItem *node){ return DividerKey(options_active_.group_by[0], node->metadata, node->sort_text) == divider_key; })) {
|
||||
QList<CollectionItem *> container_nodes = container_nodes_[0].values();
|
||||
if (std::any_of(container_nodes.begin(), container_nodes.end(), [this, divider_key](CollectionItem *node) { return DividerKey(options_active_.group_by[0], node->metadata, node->sort_text) == divider_key; })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -700,7 +708,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
||||
|
||||
QString divider_key;
|
||||
if (options_active_.show_dividers && container_level == 0) {
|
||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums));
|
||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums, options_active_.use_sort_tags));
|
||||
if (!divider_key.isEmpty()) {
|
||||
if (!divider_nodes_.contains(divider_key)) {
|
||||
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
|
||||
@@ -714,7 +722,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
||||
item->container_level = container_level;
|
||||
item->container_key = container_key;
|
||||
item->display_text = DisplayText(group_by, song);
|
||||
item->sort_text = SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums);
|
||||
item->sort_text = SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums, options_active_.use_sort_tags);
|
||||
if (!divider_key.isEmpty()) {
|
||||
item->sort_text.prepend(divider_key + QLatin1Char(' '));
|
||||
}
|
||||
@@ -1069,37 +1077,37 @@ QString CollectionModel::PrettyFormat(const Song &song) {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums) {
|
||||
QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums, const bool use_sort_tags) {
|
||||
|
||||
switch (group_by) {
|
||||
case GroupBy::AlbumArtist:
|
||||
return SortTextForName(song.effective_albumartistsort(), sort_skip_articles_for_artists);
|
||||
return SortTextForName(use_sort_tags ? song.effective_albumartistsort() : song.effective_albumartist(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Artist:
|
||||
return SortTextForName(song.effective_artistsort(), sort_skip_articles_for_artists);
|
||||
return SortTextForName(use_sort_tags ? song.effective_artistsort() : song.artist(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Album:
|
||||
return SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
|
||||
return SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums);
|
||||
case GroupBy::AlbumDisc:
|
||||
return SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::YearAlbum:
|
||||
return SortTextForNumber(std::max(0, song.year())) + song.grouping() + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
|
||||
return SortTextForYear(song.year()) + song.grouping() + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums);
|
||||
case GroupBy::YearAlbumDisc:
|
||||
return SortTextForNumber(std::max(0, song.year())) + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForYear(song.year()) + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::OriginalYearAlbum:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.grouping() + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
|
||||
return SortTextForYear(song.effective_originalyear()) + song.grouping() + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums);
|
||||
case GroupBy::OriginalYearAlbumDisc:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForYear(song.effective_originalyear()) + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::Disc:
|
||||
return SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::Year:
|
||||
return SortTextForNumber(std::max(0, song.year())) + QLatin1Char(' ');
|
||||
return SortTextForYear(song.year()) + QLatin1Char(' ');
|
||||
case GroupBy::OriginalYear:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
|
||||
return SortTextForYear(song.effective_originalyear()) + QLatin1Char(' ');
|
||||
case GroupBy::Genre:
|
||||
return SortText(song.genre());
|
||||
case GroupBy::Composer:
|
||||
return SortTextForName(song.effective_composersort(), sort_skip_articles_for_artists);
|
||||
return SortTextForName(use_sort_tags ? song.effective_composersort() : song.composer(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Performer:
|
||||
return SortTextForName(song.effective_performersort(), sort_skip_articles_for_artists);
|
||||
return SortTextForName(use_sort_tags ? song.effective_performersort() : song.performer(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Grouping:
|
||||
return SortText(song.grouping());
|
||||
case GroupBy::FileType:
|
||||
@@ -1157,14 +1165,14 @@ QString CollectionModel::SortTextForSong(const Song &song) {
|
||||
|
||||
QString CollectionModel::SortTextForYear(const int year) {
|
||||
|
||||
QString str = QString::number(year);
|
||||
const QString str = QString::number(std::max(year, 0));
|
||||
return QStringLiteral("0").repeated(qMax(0, 4 - str.length())) + str;
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortTextForBitrate(const int bitrate) {
|
||||
|
||||
QString str = QString::number(bitrate);
|
||||
const QString str = QString::number(bitrate);
|
||||
return QStringLiteral("0").repeated(qMax(0, 3 - str.length())) + str;
|
||||
|
||||
}
|
||||
@@ -1213,27 +1221,30 @@ QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song,
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
case GroupBy::AlbumDisc:
|
||||
key = PrettyAlbumDisc(song.album(), song.disc());
|
||||
key = TextOrUnknown(song.album());
|
||||
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
|
||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
case GroupBy::YearAlbum:
|
||||
key = PrettyYearAlbum(song.year(), song.album());
|
||||
key = SortTextForYear(song.year()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
case GroupBy::YearAlbumDisc:
|
||||
key = PrettyYearAlbumDisc(song.year(), song.album(), song.disc());
|
||||
key = SortTextForYear(song.year()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
|
||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
case GroupBy::OriginalYearAlbum:
|
||||
key = PrettyYearAlbum(song.effective_originalyear(), song.album());
|
||||
key = SortTextForYear(song.effective_originalyear()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
case GroupBy::OriginalYearAlbumDisc:
|
||||
key = PrettyYearAlbumDisc(song.effective_originalyear(), song.album(), song.disc());
|
||||
key = SortTextForYear(song.effective_originalyear()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
|
||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||
break;
|
||||
@@ -1241,10 +1252,10 @@ QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song,
|
||||
key = PrettyDisc(song.disc());
|
||||
break;
|
||||
case GroupBy::Year:
|
||||
key = QString::number(std::max(0, song.year()));
|
||||
key = SortTextForYear(song.year());
|
||||
break;
|
||||
case GroupBy::OriginalYear:
|
||||
key = QString::number(std::max(0, song.effective_originalyear()));
|
||||
key = SortTextForYear(song.effective_originalyear());
|
||||
break;
|
||||
case GroupBy::Genre:
|
||||
key = TextOrUnknown(song.genre());
|
||||
@@ -1336,7 +1347,7 @@ QString CollectionModel::DividerKey(const GroupBy group_by, const Song &song, co
|
||||
case GroupBy::Bitdepth:
|
||||
return SortTextForNumber(song.bitdepth());
|
||||
case GroupBy::Bitrate:
|
||||
return SortTextForNumber(song.bitrate());
|
||||
return SortTextForBitrate(song.bitrate());
|
||||
case GroupBy::None:
|
||||
case GroupBy::GroupByCount:
|
||||
return QString();
|
||||
|
||||
@@ -131,6 +131,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
show_various_artists(true),
|
||||
sort_skip_articles_for_artists(false),
|
||||
sort_skip_articles_for_albums(false),
|
||||
use_sort_tags(true),
|
||||
separate_albums_by_grouping(false) {}
|
||||
|
||||
Grouping group_by;
|
||||
@@ -139,6 +140,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
bool show_various_artists;
|
||||
bool sort_skip_articles_for_artists;
|
||||
bool sort_skip_articles_for_albums;
|
||||
bool use_sort_tags;
|
||||
bool separate_albums_by_grouping;
|
||||
CollectionFilterOptions filter_options;
|
||||
};
|
||||
@@ -178,14 +180,14 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
|
||||
// Utility functions for manipulating text
|
||||
QString DisplayText(const GroupBy group_by, const Song &song);
|
||||
static QString DisplayText(const GroupBy group_by, const Song &song);
|
||||
static QString TextOrUnknown(const QString &text);
|
||||
static QString PrettyYearAlbum(const int year, const QString &album);
|
||||
static QString PrettyAlbumDisc(const QString &album, const int disc);
|
||||
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
||||
static QString PrettyDisc(const int disc);
|
||||
static QString PrettyFormat(const Song &song);
|
||||
QString SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums);
|
||||
static QString SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums, const bool use_sort_tags);
|
||||
static QString SortText(QString text);
|
||||
static QString SortTextForName(const QString &name, const bool sort_skip_articles);
|
||||
static QString SortTextForNumber(const int number);
|
||||
|
||||
@@ -58,4 +58,3 @@ class CollectionPlaylistItem : public PlaylistItem {
|
||||
};
|
||||
|
||||
#endif // COLLECTIONPLAYLISTITEM_H
|
||||
|
||||
|
||||
@@ -101,9 +101,9 @@ void CollectionQuery::AddCompilationRequirement(const bool compilation) {
|
||||
QString CollectionQuery::GetInnerQuery() const {
|
||||
return duplicates_only_
|
||||
? QStringLiteral(" INNER JOIN (select * from duplicated_songs) dsongs "
|
||||
"ON (%songs_table.artist = dsongs.dup_artist "
|
||||
"AND %songs_table.album = dsongs.dup_album "
|
||||
"AND %songs_table.title = dsongs.dup_title) ")
|
||||
"ON (%songs_table.artist = dsongs.dup_artist "
|
||||
"AND %songs_table.album = dsongs.dup_album "
|
||||
"AND %songs_table.title = dsongs.dup_title) ")
|
||||
: QString();
|
||||
}
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@ void CollectionView::keyPressEvent(QKeyEvent *e) {
|
||||
case Qt::Key_Enter:
|
||||
case Qt::Key_Return:
|
||||
if (currentIndex().isValid()) {
|
||||
AddToPlaylist();
|
||||
Q_EMIT doubleClicked(currentIndex());
|
||||
}
|
||||
e->accept();
|
||||
break;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -75,7 +75,7 @@
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
QStringList CollectionWatcher::sValidImages = QStringList() << u"jpg"_s << u"png"_s << u"gif"_s << u"jpeg"_s;
|
||||
QStringList CollectionWatcher::sValidImages = QStringList() << u"jpg"_s << u"jpeg"_s << u"jp2"_s << u"png"_s << u"gif"_s << u"tiff"_s << u"tif"_s << u"webp"_s;
|
||||
|
||||
CollectionWatcher::CollectionWatcher(const Song::Source source,
|
||||
const SharedPtr<TaskManager> task_manager,
|
||||
@@ -261,7 +261,7 @@ void CollectionWatcher::ReloadSettings() {
|
||||
CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher, const int dir, const bool incremental, const bool ignores_mtime, const bool mark_songs_unavailable)
|
||||
: progress_(0),
|
||||
progress_max_(0),
|
||||
dir_(dir),
|
||||
dir_id_(dir),
|
||||
incremental_(incremental),
|
||||
ignores_mtime_(ignores_mtime),
|
||||
mark_songs_unavailable_(mark_songs_unavailable),
|
||||
@@ -313,6 +313,19 @@ void CollectionWatcher::ScanTransaction::AddToProgressMax(const quint64 n) {
|
||||
|
||||
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||
|
||||
if (!deleted_subdirs.isEmpty()) {
|
||||
Q_EMIT watcher_->SubdirsDeleted(deleted_subdirs);
|
||||
}
|
||||
|
||||
if (!new_subdirs.isEmpty()) {
|
||||
Q_EMIT watcher_->SubdirsDiscovered(new_subdirs);
|
||||
}
|
||||
|
||||
if (!touched_subdirs.isEmpty()) {
|
||||
Q_EMIT watcher_->SubdirsMTimeUpdated(touched_subdirs);
|
||||
touched_subdirs.clear();
|
||||
}
|
||||
|
||||
if (!deleted_songs.isEmpty()) {
|
||||
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
|
||||
Q_EMIT watcher_->SongsUnavailable(deleted_songs);
|
||||
@@ -338,34 +351,24 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||
readded_songs.clear();
|
||||
}
|
||||
|
||||
if (!new_subdirs.isEmpty()) {
|
||||
Q_EMIT watcher_->SubdirsDiscovered(new_subdirs);
|
||||
}
|
||||
|
||||
if (!touched_subdirs.isEmpty()) {
|
||||
Q_EMIT watcher_->SubdirsMTimeUpdated(touched_subdirs);
|
||||
touched_subdirs.clear();
|
||||
}
|
||||
|
||||
for (const CollectionSubdirectory &subdir : std::as_const(deleted_subdirs)) {
|
||||
if (watcher_->watched_dirs_.contains(dir_)) {
|
||||
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
|
||||
if (watcher_->watched_dirs_.contains(dir_id_)) {
|
||||
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_id_], subdir);
|
||||
}
|
||||
}
|
||||
deleted_subdirs.clear();
|
||||
|
||||
if (watcher_->monitor_) {
|
||||
// Watch the new subdirectories
|
||||
for (const CollectionSubdirectory &subdir : std::as_const(new_subdirs)) {
|
||||
if (watcher_->watched_dirs_.contains(dir_)) {
|
||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
|
||||
if (watcher_->watched_dirs_.contains(dir_id_)) {
|
||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_id_], subdir.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
new_subdirs.clear();
|
||||
|
||||
if (incremental_ || ignores_mtime_) {
|
||||
Q_EMIT watcher_->UpdateLastSeen(dir_, expire_unavailable_songs_days_);
|
||||
Q_EMIT watcher_->UpdateLastSeen(dir_id_, expire_unavailable_songs_days_);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -374,7 +377,7 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
||||
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
|
||||
|
||||
if (cached_songs_dirty_) {
|
||||
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_);
|
||||
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_id_);
|
||||
for (const Song &song : songs) {
|
||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
cached_songs_.insert(p, song);
|
||||
@@ -393,7 +396,7 @@ SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QStri
|
||||
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QString &path) {
|
||||
|
||||
if (cached_songs_missing_fingerprint_dirty_) {
|
||||
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_);
|
||||
const SongList songs = watcher_->backend_->SongsWithMissingFingerprint(dir_id_);
|
||||
for (const Song &song : songs) {
|
||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
cached_songs_missing_fingerprint_.insert(p, song);
|
||||
@@ -408,7 +411,7 @@ bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QS
|
||||
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingLoudnessCharacteristics(const QString &path) {
|
||||
|
||||
if (cached_songs_missing_loudness_characteristics_dirty_) {
|
||||
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_);
|
||||
const SongList songs = watcher_->backend_->SongsWithMissingLoudnessCharacteristics(dir_id_);
|
||||
for (const Song &song : songs) {
|
||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
cached_songs_missing_loudness_characteristics_.insert(p, song);
|
||||
@@ -430,7 +433,7 @@ void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const CollectionSubdire
|
||||
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||
}
|
||||
|
||||
return std::any_of(known_subdirs_.begin(), known_subdirs_.end(), [path](const CollectionSubdirectory &subdir) { return subdir.path == path && subdir.mtime != 0; });
|
||||
@@ -440,7 +443,7 @@ bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||
}
|
||||
|
||||
CollectionSubdirectoryList ret;
|
||||
@@ -457,7 +460,7 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdi
|
||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
||||
|
||||
if (known_subdirs_dirty_) {
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||
}
|
||||
|
||||
return known_subdirs_;
|
||||
@@ -494,7 +497,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
||||
const quint64 files_count = FilesCountForPath(&transaction, dir.path);
|
||||
transaction.SetKnownSubdirs(subdirs);
|
||||
transaction.AddToProgressMax(files_count);
|
||||
ScanSubdirectory(dir.path, CollectionSubdirectory(), files_count, &transaction);
|
||||
ScanSubdirectory(dir, dir.path, CollectionSubdirectory(), files_count, &transaction);
|
||||
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
||||
}
|
||||
else {
|
||||
@@ -512,7 +515,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
||||
transaction.AddToProgressMax(files_count);
|
||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
}
|
||||
if (!stop_or_abort_requested()) {
|
||||
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
||||
@@ -524,9 +527,10 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
|
||||
void CollectionWatcher::ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, ScanTransaction *t, const bool force_noincremental) {
|
||||
|
||||
const QFileInfo path_info(path);
|
||||
const qint64 path_mtime = path_info.exists() && path_info.lastModified().isValid() ? path_info.lastModified().toSecsSinceEpoch() : 0;
|
||||
|
||||
if (path_info.isSymLink()) {
|
||||
const QString real_path = path_info.symLinkTarget();
|
||||
@@ -536,8 +540,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
return;
|
||||
}
|
||||
// Do not scan symlinked dirs that are already in collection
|
||||
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
|
||||
if (real_path.startsWith(dir.path)) {
|
||||
for (const CollectionDirectory &i : std::as_const(watched_dirs_)) {
|
||||
if (real_path.startsWith(i.path)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -563,7 +567,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() && subdir.mtime == path_info.lastModified().toSecsSinceEpoch() && !songs_missing_fingerprint && !songs_missing_loudness_characteristics) {
|
||||
if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() && path_mtime != 0 && subdir.mtime == path_mtime && !songs_missing_fingerprint && !songs_missing_loudness_characteristics) {
|
||||
// The directory hasn't changed since last time
|
||||
t->AddToProgress(files_count);
|
||||
return;
|
||||
@@ -578,53 +582,52 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
const CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
||||
for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
|
||||
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
|
||||
ScanSubdirectory(prev_subdir.path, prev_subdir, 0, t, true);
|
||||
ScanSubdirectory(dir, prev_subdir.path, prev_subdir, 0, t, true);
|
||||
}
|
||||
}
|
||||
|
||||
// First we "quickly" get a list of the files in the directory that we think might be music. While we're here, we also look for new subdirectories and possible album artwork.
|
||||
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||
while (it.hasNext()) {
|
||||
if (path_info.exists()) {
|
||||
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||
while (it.hasNext()) {
|
||||
|
||||
if (stop_or_abort_requested()) return;
|
||||
if (stop_or_abort_requested()) return;
|
||||
|
||||
const QString child_filepath = it.next();
|
||||
const QFileInfo child_fileinfo(child_filepath);
|
||||
const QString child_filepath = it.next();
|
||||
const QFileInfo child_fileinfo(child_filepath);
|
||||
|
||||
if (child_fileinfo.isSymLink()) {
|
||||
QStorageInfo storage_info(child_fileinfo.symLinkTarget());
|
||||
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
|
||||
qLog(Warning) << "Ignoring symbolic link" << child_filepath << "which links to" << child_fileinfo.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
|
||||
continue;
|
||||
if (child_fileinfo.isSymLink()) {
|
||||
const QStorageInfo storage_info(child_fileinfo.symLinkTarget());
|
||||
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
|
||||
qLog(Warning) << "Ignoring symbolic link" << child_filepath << "which links to" << child_fileinfo.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (child_fileinfo.isDir()) {
|
||||
if (!t->HasSeenSubdir(child_filepath)) {
|
||||
// We haven't seen this subdirectory before - add it to a list, and later we'll tell the backend about it and scan it.
|
||||
CollectionSubdirectory new_subdir;
|
||||
new_subdir.directory_id = -1;
|
||||
new_subdir.path = child_filepath;
|
||||
new_subdir.mtime = child_fileinfo.lastModified().toSecsSinceEpoch();
|
||||
my_new_subdirs << new_subdir;
|
||||
}
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else {
|
||||
QString ext_part(ExtensionPart(child_filepath));
|
||||
QString dir_part(DirectoryPart(child_filepath));
|
||||
if (Song::kRejectedExtensions.contains(child_fileinfo.suffix(), Qt::CaseInsensitive) || child_fileinfo.baseName() == "qt_temp"_L1) {
|
||||
if (child_fileinfo.isDir()) {
|
||||
if (!t->HasSeenSubdir(child_filepath)) {
|
||||
// We haven't seen this subdirectory before - add it to a list, and later we'll tell the backend about it and scan it.
|
||||
CollectionSubdirectory new_subdir;
|
||||
new_subdir.directory_id = -1;
|
||||
new_subdir.path = child_filepath;
|
||||
new_subdir.mtime = child_fileinfo.exists() && child_fileinfo.lastModified().isValid() ? child_fileinfo.lastModified().toSecsSinceEpoch() : 0;
|
||||
my_new_subdirs << new_subdir;
|
||||
}
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else if (sValidImages.contains(ext_part)) {
|
||||
album_art[dir_part] << child_filepath;
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else if (tagreader_client_->IsMediaFileBlocking(child_filepath)) {
|
||||
files_on_disk << child_filepath;
|
||||
}
|
||||
else {
|
||||
t->AddToProgress(1);
|
||||
const QString ext_part = ExtensionPart(child_filepath);
|
||||
const QString dir_part = DirectoryPart(child_filepath);
|
||||
if (Song::kRejectedExtensions.contains(child_fileinfo.suffix(), Qt::CaseInsensitive) || child_fileinfo.baseName() == "qt_temp"_L1) {
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else if (sValidImages.contains(ext_part)) {
|
||||
album_art[dir_part] << child_filepath;
|
||||
t->AddToProgress(1);
|
||||
}
|
||||
else {
|
||||
files_on_disk << child_filepath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -632,27 +635,27 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
if (stop_or_abort_requested()) return;
|
||||
|
||||
// Ask the database for a list of files in this directory
|
||||
SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
||||
const SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
||||
|
||||
QSet<QString> cues_processed;
|
||||
|
||||
// Now compare the list from the database with the list of files on disk
|
||||
QStringList files_on_disk_copy = files_on_disk;
|
||||
const QStringList files_on_disk_copy = files_on_disk;
|
||||
for (const QString &file : files_on_disk_copy) {
|
||||
|
||||
if (stop_or_abort_requested()) return;
|
||||
|
||||
// Associated CUE
|
||||
QString new_cue = CueParser::FindCueFilename(file);
|
||||
const QString new_cue = CueParser::FindCueFilename(file);
|
||||
|
||||
SongList matching_songs;
|
||||
if (FindSongsByPath(songs_in_db, file, &matching_songs)) { // Found matching song in DB by path.
|
||||
|
||||
Song matching_song = matching_songs.first();
|
||||
const Song matching_song = matching_songs.first();
|
||||
|
||||
// The song is in the database and still on disk.
|
||||
// Check the mtime to see if it's been changed since it was added.
|
||||
QFileInfo fileinfo(file);
|
||||
const QFileInfo fileinfo(file);
|
||||
|
||||
if (!fileinfo.exists()) {
|
||||
// Partially fixes race condition - if file was removed between being added to the list and now.
|
||||
@@ -706,8 +709,14 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
qLog(Debug) << file << "is missing EBU R 128 loudness characteristics.";
|
||||
}
|
||||
|
||||
// If the song is unavailable and nothing has changed, just mark it as available without re-scanning
|
||||
// For CUE files with multiple sections, all sections share the same file and would have the same availability status
|
||||
if (matching_song.unavailable() && !changed && !missing_fingerprint && !missing_loudness_characteristics) {
|
||||
qLog(Debug) << "Unavailable song" << file << "restored without re-scanning.";
|
||||
t->readded_songs << matching_songs;
|
||||
}
|
||||
// The song's changed or missing fingerprint - create fingerprint and reread the metadata from file.
|
||||
if (t->ignores_mtime() || changed || missing_fingerprint || missing_loudness_characteristics) {
|
||||
else if (t->ignores_mtime() || changed || missing_fingerprint || missing_loudness_characteristics) {
|
||||
|
||||
QString fingerprint;
|
||||
#ifdef HAVE_SONGFINGERPRINTING
|
||||
@@ -721,19 +730,15 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
#endif
|
||||
|
||||
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
|
||||
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t);
|
||||
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t)) {
|
||||
files_on_disk.removeAll(file);
|
||||
}
|
||||
}
|
||||
else { // If CUE associated.
|
||||
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing has changed - mark the song available without re-scanning
|
||||
else if (matching_song.unavailable()) {
|
||||
qLog(Debug) << "Unavailable song" << file << "restored.";
|
||||
t->readded_songs << matching_songs;
|
||||
}
|
||||
|
||||
}
|
||||
else { // Search the DB by fingerprint.
|
||||
QString fingerprint;
|
||||
@@ -750,7 +755,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
|
||||
// The song is in the database and still on disk.
|
||||
// Check the mtime to see if it's been changed since it was added.
|
||||
QFileInfo fileinfo(file);
|
||||
const QFileInfo fileinfo(file);
|
||||
if (!fileinfo.exists()) {
|
||||
// Partially fixes race condition - if file was removed between being added to the list and now.
|
||||
files_on_disk.removeAll(file);
|
||||
@@ -761,7 +766,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
// Make sure the songs aren't deleted, as they still exist elsewhere with a different file path.
|
||||
bool matching_songs_has_cue = false;
|
||||
for (const Song &matching_song : std::as_const(matching_songs)) {
|
||||
QString matching_filename = matching_song.url().toLocalFile();
|
||||
const QString matching_filename = matching_song.url().toLocalFile();
|
||||
if (!t->files_changed_path_.contains(matching_filename)) {
|
||||
t->files_changed_path_ << matching_filename;
|
||||
qLog(Debug) << matching_filename << "has changed path to" << file;
|
||||
@@ -784,7 +789,9 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
const QUrl art_automatic = ArtForSong(file, album_art);
|
||||
|
||||
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
|
||||
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t);
|
||||
if (!UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t)) {
|
||||
files_on_disk.removeAll(file);
|
||||
}
|
||||
}
|
||||
else { // If CUE associated.
|
||||
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
||||
@@ -795,6 +802,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
|
||||
const SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
|
||||
if (songs.isEmpty()) {
|
||||
files_on_disk.removeAll(file);
|
||||
t->AddToProgress(1);
|
||||
continue;
|
||||
}
|
||||
@@ -805,7 +813,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
const QUrl art_automatic = ArtForSong(file, album_art);
|
||||
|
||||
for (Song song : songs) {
|
||||
song.set_directory_id(t->dir());
|
||||
song.set_directory_id(t->dir_id());
|
||||
if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic);
|
||||
t->new_songs << song;
|
||||
}
|
||||
@@ -823,27 +831,26 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
||||
}
|
||||
}
|
||||
|
||||
// Add this subdir to the new or touched list
|
||||
// Add, update or delete subdir
|
||||
CollectionSubdirectory updated_subdir;
|
||||
updated_subdir.directory_id = t->dir();
|
||||
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
|
||||
updated_subdir.directory_id = t->dir_id();
|
||||
updated_subdir.mtime = path_mtime;
|
||||
updated_subdir.path = path;
|
||||
|
||||
if (subdir.directory_id == -1) {
|
||||
if (!path_info.exists() && updated_subdir.path != dir.path) {
|
||||
t->deleted_subdirs << updated_subdir;
|
||||
}
|
||||
else if (subdir.directory_id == -1) {
|
||||
t->new_subdirs << updated_subdir;
|
||||
}
|
||||
else {
|
||||
else if (subdir.mtime != updated_subdir.mtime) {
|
||||
t->touched_subdirs << updated_subdir;
|
||||
}
|
||||
|
||||
if (updated_subdir.mtime == 0) { // CollectionSubdirectory deleted, mark it for removal from the watcher.
|
||||
t->deleted_subdirs << updated_subdir;
|
||||
}
|
||||
|
||||
// Recurse into the new subdirs that we found
|
||||
for (const CollectionSubdirectory &my_new_subdir : std::as_const(my_new_subdirs)) {
|
||||
if (stop_or_abort_requested()) return;
|
||||
ScanSubdirectory(my_new_subdir.path, my_new_subdir, 0, t, true);
|
||||
ScanSubdirectory(dir, my_new_subdir.path, my_new_subdir, 0, t, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -875,7 +882,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
|
||||
QSet<int> used_ids;
|
||||
for (Song new_cue_song : songs) {
|
||||
new_cue_song.set_source(source_);
|
||||
new_cue_song.set_directory_id(t->dir());
|
||||
new_cue_song.set_directory_id(t->dir_id());
|
||||
PerformEBUR128Analysis(new_cue_song);
|
||||
new_cue_song.set_fingerprint(fingerprint);
|
||||
|
||||
@@ -901,7 +908,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
bool CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
const QString &fingerprint,
|
||||
const SongList &matching_songs,
|
||||
const QUrl &art_automatic,
|
||||
@@ -922,7 +929,7 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
const TagReaderResult result = tagreader_client_->ReadFileBlocking(file, &song_on_disk);
|
||||
if (result.success() && song_on_disk.is_valid()) {
|
||||
song_on_disk.set_source(source_);
|
||||
song_on_disk.set_directory_id(t->dir());
|
||||
song_on_disk.set_directory_id(t->dir_id());
|
||||
song_on_disk.set_id(matching_song.id());
|
||||
PerformEBUR128Analysis(song_on_disk);
|
||||
song_on_disk.set_fingerprint(fingerprint);
|
||||
@@ -931,6 +938,8 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
||||
AddChangedSong(file, matching_song, song_on_disk, t);
|
||||
}
|
||||
|
||||
return result.success() && song_on_disk.is_valid();
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const {
|
||||
@@ -1199,12 +1208,13 @@ void CollectionWatcher::DirectoryChanged(const QString &subdir) {
|
||||
|
||||
void CollectionWatcher::RescanPathsNow() {
|
||||
|
||||
const QList<int> dirs = rescan_queue_.keys();
|
||||
for (const int dir : dirs) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
ScanTransaction transaction(this, dir, false, false, mark_songs_unavailable_);
|
||||
const QList<int> dir_ids = rescan_queue_.keys();
|
||||
for (const int dir_id : dir_ids) {
|
||||
|
||||
const QStringList paths = rescan_queue_.value(dir);
|
||||
if (stop_or_abort_requested()) break;
|
||||
ScanTransaction transaction(this, dir_id, false, false, mark_songs_unavailable_);
|
||||
|
||||
const QStringList paths = rescan_queue_.value(dir_id);
|
||||
|
||||
QMap<QString, quint64> subdir_files_count;
|
||||
for (const QString &path : paths) {
|
||||
@@ -1215,11 +1225,14 @@ void CollectionWatcher::RescanPathsNow() {
|
||||
|
||||
for (const QString &path : paths) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
if (!subdir_mapping_.contains(path)) {
|
||||
continue;
|
||||
}
|
||||
CollectionSubdirectory subdir;
|
||||
subdir.directory_id = dir;
|
||||
subdir.directory_id = dir_id;
|
||||
subdir.mtime = 0;
|
||||
subdir.path = path;
|
||||
ScanSubdirectory(path, subdir, subdir_files_count[path], &transaction);
|
||||
ScanSubdirectory(subdir_mapping_[path], path, subdir, subdir_files_count[path], &transaction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1344,11 +1357,13 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
|
||||
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_);
|
||||
CollectionSubdirectoryList subdirs = transaction.GetAllSubdirs();
|
||||
|
||||
if (subdirs.isEmpty()) {
|
||||
qLog(Debug) << "Collection directory wasn't in subdir list.";
|
||||
const bool has_collection_root_dir = std::any_of(subdirs.begin(), subdirs.end(), [&dir](const CollectionSubdirectory &subdir) { return subdir.path == dir.path; });
|
||||
if (!has_collection_root_dir) {
|
||||
qLog(Debug) << "Collection directory wasn't in subdir list, re-adding";
|
||||
CollectionSubdirectory subdir;
|
||||
subdir.path = dir.path;
|
||||
subdir.directory_id = dir.id;
|
||||
subdir.path = dir.path;
|
||||
subdir.mtime = 0;
|
||||
subdirs << subdir;
|
||||
}
|
||||
|
||||
@@ -1358,7 +1373,7 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
|
||||
|
||||
for (const CollectionSubdirectory &subdir : std::as_const(subdirs)) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
ScanSubdirectory(subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
ScanSubdirectory(dir, subdir.path, subdir, subdir_files_count[subdir.path], &transaction);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1459,6 +1474,8 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
|
||||
QStringList scanned_paths;
|
||||
for (const Song &song : songs) {
|
||||
if (stop_or_abort_requested()) break;
|
||||
if (!watched_dirs_.contains(song.directory_id())) continue;
|
||||
const CollectionDirectory dir = watched_dirs_[song.directory_id()];
|
||||
const QString song_path = song.url().toLocalFile().section(u'/', 0, -2);
|
||||
if (scanned_paths.contains(song_path)) continue;
|
||||
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
|
||||
@@ -1468,7 +1485,7 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
|
||||
if (subdir.path != song_path) continue;
|
||||
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
|
||||
const quint64 files_count = FilesCountForPath(&transaction, subdir.path);
|
||||
ScanSubdirectory(song_path, subdir, files_count, &transaction);
|
||||
ScanSubdirectory(dir, song_path, subdir, files_count, &transaction);
|
||||
scanned_paths << subdir.path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -85,6 +85,7 @@ class CollectionWatcher : public QObject {
|
||||
void SongsReadded(const SongList &songs, const bool unavailable = false);
|
||||
void SubdirsDiscovered(const CollectionSubdirectoryList &subdirs);
|
||||
void SubdirsMTimeUpdated(const CollectionSubdirectoryList &subdirs);
|
||||
void SubdirsDeleted(const CollectionSubdirectoryList &subdirs);
|
||||
void CompilationsNeedUpdating();
|
||||
void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
|
||||
void ExitFinished();
|
||||
@@ -122,7 +123,7 @@ class CollectionWatcher : public QObject {
|
||||
// Emits the signals for new & deleted songs etc and clears the lists. This causes the new stuff to be updated on UI.
|
||||
void CommitNewOrUpdatedSongs();
|
||||
|
||||
int dir() const { return dir_; }
|
||||
int dir_id() const { return dir_id_; }
|
||||
bool is_incremental() const { return incremental_; }
|
||||
bool ignores_mtime() const { return ignores_mtime_; }
|
||||
|
||||
@@ -137,13 +138,13 @@ class CollectionWatcher : public QObject {
|
||||
QStringList files_changed_path_;
|
||||
|
||||
private:
|
||||
ScanTransaction &operator=(const ScanTransaction&) { return *this; }
|
||||
ScanTransaction &operator=(const ScanTransaction &transaction) { Q_UNUSED(transaction); return *this; }
|
||||
|
||||
int task_id_;
|
||||
quint64 progress_;
|
||||
quint64 progress_max_;
|
||||
|
||||
int dir_;
|
||||
int dir_id_;
|
||||
// Incremental scan enters a directory only if it has changed since the last scan.
|
||||
bool incremental_;
|
||||
// This type of scan updates every file in a folder that's being scanned.
|
||||
@@ -179,7 +180,7 @@ class CollectionWatcher : public QObject {
|
||||
void IncrementalScanNow();
|
||||
void FullScanNow();
|
||||
void RescanPathsNow();
|
||||
void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
|
||||
void ScanSubdirectory(const CollectionDirectory &dir, const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
|
||||
void RescanSongs(const SongList &songs);
|
||||
|
||||
private:
|
||||
@@ -202,7 +203,7 @@ class CollectionWatcher : public QObject {
|
||||
// Updates the sections of a cue associated and altered (according to mtime) media file during a scan.
|
||||
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &art_automatic, const SongList &old_cue_songs, ScanTransaction *t) const;
|
||||
// Updates a single non-cue associated and altered (according to mtime) song during a scan.
|
||||
void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
|
||||
bool UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
|
||||
// Scans a single media file that's present on the disk but not yet in the collection.
|
||||
// It may result in a multiple files added to the collection when the media file has many sections (like a CUE related media file).
|
||||
SongList ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed) const;
|
||||
@@ -261,7 +262,6 @@ class CollectionWatcher : public QObject {
|
||||
static QStringList sValidImages;
|
||||
|
||||
qint64 last_scan_time_;
|
||||
|
||||
};
|
||||
|
||||
inline QString CollectionWatcher::NoExtensionPart(const QString &fileName) {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#cmakedefine USE_INSTALL_PREFIX
|
||||
|
||||
#cmakedefine BUILD_FOR_MAC_APP_STORE
|
||||
|
||||
#cmakedefine HAVE_BACKTRACE
|
||||
#cmakedefine HAVE_ALSA
|
||||
#cmakedefine HAVE_PULSE
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef APPEARANCESETTINGS_H
|
||||
#define APPEARANCESETTINGS_H
|
||||
@@ -70,6 +70,6 @@ enum class BackgroundImagePosition {
|
||||
BottomRight = 5
|
||||
};
|
||||
|
||||
} // namespace
|
||||
} // namespace AppearanceSettings
|
||||
|
||||
#endif // APPEARANCESETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef BACKENDSETTINGS_H
|
||||
#define BACKENDSETTINGS_H
|
||||
@@ -63,6 +63,6 @@ constexpr qint64 kDefaultBufferDuration = 4000;
|
||||
constexpr double kDefaultBufferLowWatermark = 0.33;
|
||||
constexpr double kDefaultBufferHighWatermark = 0.99;
|
||||
|
||||
} // namespace
|
||||
} // namespace BackendSettings
|
||||
|
||||
#endif // BACKENDSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef BEHAVIOURSETTINGS_H
|
||||
#define BEHAVIOURSETTINGS_H
|
||||
@@ -71,6 +71,6 @@ constexpr char kDoubleClickPlaylistAddMode[] = "doubleclick_playlist_addmode";
|
||||
constexpr char kSeekStepSec[] = "seek_step_sec";
|
||||
constexpr char kVolumeIncrement[] = "volume_increment";
|
||||
|
||||
} // namespace
|
||||
} // namespace BehaviourSettings
|
||||
|
||||
#endif // BEHAVIOURSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COLLECTIONSETTINGS_H
|
||||
#define COLLECTIONSETTINGS_H
|
||||
@@ -37,7 +37,7 @@ constexpr char kPrettyCovers[] = "pretty_covers";
|
||||
constexpr char kVariousArtists[] = "various_artists";
|
||||
constexpr char kSkipArticlesForArtists[] = "skip_articles_for_artists";
|
||||
constexpr char kSkipArticlesForAlbums[] = "skip_articles_for_albums";
|
||||
constexpr char kShowSortText[] = "show_sort_text";
|
||||
constexpr char kUseSortTags[] = "use_short_tags";
|
||||
constexpr char kSettingsCacheSize[] = "cache_size";
|
||||
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
|
||||
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";
|
||||
@@ -59,6 +59,6 @@ enum class CacheSizeUnit {
|
||||
TB
|
||||
};
|
||||
|
||||
} // namespace
|
||||
} // namespace CollectionSettings
|
||||
|
||||
#endif // COLLECTIONSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef CONTEXTSETTINGS_H
|
||||
#define CONTEXTSETTINGS_H
|
||||
@@ -43,6 +43,6 @@ constexpr char kSettingsSummaryFmt[] = "SummaryFmt";
|
||||
constexpr char kDefaultFontFamily[] = "Noto Sans";
|
||||
constexpr qreal kDefaultFontSizeHeadline = 11;
|
||||
|
||||
} // namespace
|
||||
} // namespace ContextSettings
|
||||
|
||||
#endif // CONTEXTSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef COVERSSETTINGS_H
|
||||
#define COVERSSETTINGS_H
|
||||
@@ -32,6 +32,6 @@ constexpr char kSaveOverwrite[] = "save_overwrite";
|
||||
constexpr char kSaveLowercase[] = "save_lowercase";
|
||||
constexpr char kSaveReplaceSpaces[] = "save_replace_spaces";
|
||||
|
||||
} // namespace
|
||||
} // namespace CoversSettings
|
||||
|
||||
#endif // COVERSSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILEFILTERCONSTANTS_H
|
||||
#define FILEFILTERCONSTANTS_H
|
||||
@@ -27,13 +27,13 @@ constexpr char kAllFilesFilterSpec[] = QT_TRANSLATE_NOOP("FileFilter", "All File
|
||||
constexpr char kFileFilter[] =
|
||||
"*.wav *.flac *.wv *.ogg *.oga *.opus *.spx *.ape *.mpc "
|
||||
"*.mp2 *.mp3 *.m4a *.mp4 *.aac *.asf *.asx *.wma "
|
||||
"*.aif *.aiff *.mka *.tta *.dsf *.dsd "
|
||||
"*.aif *.aiff *.mka *.tta *.dsf *.dsd *.webm "
|
||||
"*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini "
|
||||
"*.ac3 *.dts "
|
||||
"*.mod *.s3m *.xm *.it "
|
||||
"*.spc *.vgm";
|
||||
|
||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
|
||||
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
|
||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm *.webp)");
|
||||
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm *.webp)");
|
||||
|
||||
#endif // FILEFILTERCONSTANTS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILENAMECONSTANTS_H
|
||||
#define FILENAMECONSTANTS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILESYSTEMCONSTANTS_H
|
||||
#define FILESYSTEMCONSTANTS_H
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user