Compare commits
173 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f9de8e1d9 | |||
| 3d10414a88 | |||
| c673fd2a76 | |||
| f92419f20b | |||
| 32eee8f868 | |||
| 2cd7d6026e | |||
| 4a1c165295 | |||
| 0ac4c93a4e | |||
| 010e18ba91 | |||
| ef1ac290cd | |||
| 484ce3f737 | |||
| 49cd7a6210 | |||
| b65f33f6bd | |||
| 09c49423bf | |||
| ea18b97348 | |||
| 58dd0877e7 | |||
| e9425ba17b | |||
| 32d663e58f | |||
| a69024c0be | |||
| 81d5f57d13 | |||
| 40fadd640f | |||
|
|
1994c367c9 | ||
|
|
4915db55ba | ||
|
|
ce06115557 | ||
|
|
89d1ac8f20 | ||
|
|
891b635c64 | ||
|
|
f37b1099f3 | ||
|
|
626dd48730 | ||
|
|
6f7b8ab162 | ||
|
|
3416ede211 | ||
|
|
f8bb69ec65 | ||
|
|
64540ef6f9 | ||
|
|
cd013db33b | ||
|
|
4f554f5d5f | ||
|
|
326fe84e8a | ||
|
|
1bded170a2 | ||
|
|
a71e5b170b | ||
|
|
ea629aedd1 | ||
|
|
610b458196 | ||
|
|
ad285a91f2 | ||
|
|
6400f903e8 | ||
|
|
83d5f3d8f2 | ||
|
|
582b8e8076 | ||
|
|
030908f6ac | ||
|
|
34ae443548 | ||
|
|
1c9e99e776 | ||
|
|
4e6459b977 | ||
|
|
d2b5359fa9 | ||
|
|
1d82977441 | ||
|
|
17519076f5 | ||
|
|
e8d9e1172f | ||
|
|
aac8d4e68b | ||
|
|
0e28e800b3 | ||
|
|
cf84bc29ab | ||
|
|
afc3effc9d | ||
|
|
370bebff5f | ||
|
|
db410cc257 | ||
|
|
20a9946e51 | ||
|
|
b6c8ff19af | ||
|
|
80d058af10 | ||
|
|
da2f28811a | ||
|
|
0bfa736081 | ||
|
|
1392bcbbe1 | ||
|
|
11705889f1 | ||
|
|
604dd2dbde | ||
|
|
25065ba98f | ||
|
|
7b16ec62bb | ||
|
|
d8f31592b9 | ||
|
|
80bb0f476d | ||
|
|
b7222ac85c | ||
|
|
241bca0828 | ||
|
|
90d86b10a3 | ||
|
|
4130c6670f | ||
|
|
8d262959c1 | ||
|
|
b9b70399d8 | ||
|
|
527ccd212a | ||
|
|
4a5afbeb1e | ||
|
|
63c14e014b | ||
|
|
801658c6b9 | ||
|
|
16fe665295 | ||
|
|
2bb0dbada2 | ||
|
|
2cd9498469 | ||
|
|
d1ee27fff9 | ||
|
|
91adf5ba32 | ||
|
|
d68f464269 | ||
|
|
c684a95f89 | ||
|
|
1d03bb2178 | ||
|
|
39f9128ecf | ||
|
|
ca2e802239 | ||
|
|
9a513a9a56 | ||
|
|
1c2e87b741 | ||
|
|
fe4d9979ce | ||
|
|
d8ae790ebf | ||
|
|
ac31d79294 | ||
|
|
4ffebd77b1 | ||
|
|
6682efae2f | ||
|
|
480161c6b7 | ||
|
|
a8ba420d72 | ||
|
|
fc0ec91652 | ||
|
|
0701b97324 | ||
|
|
3867932e1e | ||
|
|
e2907f6051 | ||
|
|
0827ec7f16 | ||
|
|
24d2adf363 | ||
|
|
592729d00b | ||
|
|
c4a564bb56 | ||
|
|
812a02a3a1 | ||
|
|
944936914b | ||
|
|
e7c901d4f3 | ||
|
|
8e996119af | ||
|
|
4348a654ca | ||
|
|
f0be1c782a | ||
|
|
e9898d08bc | ||
|
|
1ad13cd3b0 | ||
|
|
5c640e0e36 | ||
|
|
059def8d0c | ||
|
|
cf15a1f423 | ||
|
|
5d35b0eedd | ||
|
|
5fcb71d08f | ||
|
|
15c2237d4a | ||
|
|
93af866185 | ||
|
|
109ff90401 | ||
|
|
d4b06289c3 | ||
|
|
4351c555a0 | ||
|
|
ce4db40983 | ||
|
|
d37fb7410c | ||
|
|
f1cdd71494 | ||
|
|
000ba997fb | ||
|
|
579efffd14 | ||
|
|
3a098c8a0c | ||
|
|
5bce0ae87f | ||
|
|
afe6967c46 | ||
|
|
e91bab6d42 | ||
|
|
5a64247761 | ||
|
|
9ed89afdb2 | ||
|
|
0ac338026c | ||
|
|
4e5f84a7b7 | ||
|
|
320a3c6815 | ||
|
|
72dd1d783a | ||
|
|
d2205cfe81 | ||
|
|
5830f247f6 | ||
|
|
8b4c57d933 | ||
|
|
67cec09176 | ||
|
|
2df658e1f3 | ||
|
|
f3bc9b151c | ||
|
|
b06b59d0c5 | ||
|
|
99d75ade06 | ||
|
|
3f63246068 | ||
|
|
b205a5f964 | ||
|
|
aeaef12dd4 | ||
|
|
02d76f17f7 | ||
|
|
e4e12c6fa6 | ||
|
|
270ae6085b | ||
|
|
7065a405a5 | ||
|
|
d8c72c3dd9 | ||
|
|
b65502e167 | ||
|
|
132f8df853 | ||
|
|
12e3cffe63 | ||
|
|
56a637682d | ||
|
|
d9b105f89e | ||
|
|
bd6b45e43f | ||
|
|
539172fb70 | ||
|
|
ebd92b3a7f | ||
|
|
b00ae5b210 | ||
|
|
c8e3cf981b | ||
|
|
038f69779f | ||
|
|
a4de7559ac | ||
|
|
0537b072fe | ||
|
|
2657c9f96a | ||
|
|
7e178b1f1a | ||
|
|
dd8513d02c | ||
|
|
5f0175094b | ||
|
|
b4c5b9e1e1 |
@@ -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
|
||||
86
.github/workflows/build.yml
vendored
86
.github/workflows/build.yml
vendored
@@ -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
|
||||
@@ -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: |
|
||||
@@ -511,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
|
||||
@@ -528,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: |
|
||||
@@ -542,7 +542,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
|
||||
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -596,10 +596,10 @@ jobs:
|
||||
qt6-l10n-tools
|
||||
rapidjson-dev
|
||||
- name: Install KDSingleApplication
|
||||
if: matrix.ubuntu_version != 'noble' && matrix.ubuntu_version != 'plucky'
|
||||
if: matrix.ubuntu_version != 'noble'
|
||||
run: apt install -y libkdsingleapplication-qt6-dev
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.ubuntu_version == 'noble' || matrix.ubuntu_version == 'plucky'
|
||||
if: matrix.ubuntu_version == 'noble'
|
||||
run: |
|
||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||
cd KDSingleApplication
|
||||
@@ -607,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
|
||||
@@ -624,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: |
|
||||
@@ -639,7 +639,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
|
||||
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -699,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
|
||||
@@ -735,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.4
|
||||
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
|
||||
@@ -760,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.1
|
||||
uses: vmactions/openbsd-vm@v1.3.1
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
@@ -787,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 }}
|
||||
@@ -826,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 }}
|
||||
@@ -890,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'
|
||||
@@ -977,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
|
||||
@@ -1080,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
|
||||
@@ -1310,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'
|
||||
@@ -1321,10 +1327,6 @@ jobs:
|
||||
shell: cmd
|
||||
run: choco install --no-progress rsync
|
||||
|
||||
- name: Set SSH command
|
||||
shell: bash
|
||||
run: echo "ssh_command=/c/ProgramData/chocolatey/lib/rsync/tools/$(ls -1 /c/ProgramData/chocolatey/lib/rsync/tools/ | grep -v '\.zip$' | grep '^cwrsync' | tail -1)/bin/ssh.exe" >> $GITHUB_ENV
|
||||
|
||||
- name: Cleanup PATH
|
||||
uses: egor-tensin/cleanup-path@v4
|
||||
with:
|
||||
@@ -1409,7 +1411,7 @@ jobs:
|
||||
vsversion: 2022
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1623,7 +1625,7 @@ jobs:
|
||||
- name: rsync
|
||||
if: steps.set-upload-path.outputs.upload_path != ''
|
||||
shell: bash
|
||||
run: rsync -e "${{env.ssh_command}} -p ${{secrets.SSH_PORT}} -o StrictHostKeyChecking=no" -var build/StrawberrySetup*.exe ${{secrets.SSH_USER}}@${{secrets.SSH_HOST}}:${{steps.set-upload-path.outputs.upload_path}}/
|
||||
run: rsync -e "/c/ProgramData/chocolatey/lib/rsync/tools/bin/ssh.exe -p ${{secrets.SSH_PORT}} -o StrictHostKeyChecking=no" -var build/StrawberrySetup*.exe ${{secrets.SSH_USER}}@${{secrets.SSH_HOST}}:${{steps.set-upload-path.outputs.upload_path}}/
|
||||
|
||||
|
||||
upload:
|
||||
@@ -1642,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
|
||||
@@ -1690,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
|
||||
@@ -1698,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/**
|
||||
|
||||
6
3rdparty/discord-rpc/CMakeLists.txt
vendored
6
3rdparty/discord-rpc/CMakeLists.txt
vendored
@@ -37,5 +37,9 @@ if(WIN32)
|
||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||
endif()
|
||||
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
if(TARGET RapidJSON::RapidJSON)
|
||||
target_link_libraries(discord-rpc PRIVATE RapidJSON::RapidJSON)
|
||||
elseif(RapidJSON_INCLUDE_DIRS)
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
endif()
|
||||
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
2
3rdparty/discord-rpc/discord_connection.h
vendored
2
3rdparty/discord-rpc/discord_connection.h
vendored
@@ -35,7 +35,7 @@ int GetProcessId();
|
||||
|
||||
struct BaseConnection {
|
||||
static BaseConnection *Create();
|
||||
static void Destroy(BaseConnection *&);
|
||||
static void Destroy(BaseConnection*&);
|
||||
bool isOpen = false;
|
||||
bool Open();
|
||||
bool Close();
|
||||
|
||||
@@ -39,11 +39,11 @@ int GetProcessId() {
|
||||
}
|
||||
|
||||
struct BaseConnectionUnix : public BaseConnection {
|
||||
int sock { -1 };
|
||||
int sock{ -1 };
|
||||
};
|
||||
|
||||
static BaseConnectionUnix Connection;
|
||||
static sockaddr_un PipeAddr {};
|
||||
static sockaddr_un PipeAddr{};
|
||||
#ifdef MSG_NOSIGNAL
|
||||
static int MsgFlags = MSG_NOSIGNAL;
|
||||
#else
|
||||
@@ -105,7 +105,7 @@ bool BaseConnection::Open() {
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
12
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
12
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
@@ -38,7 +38,7 @@ int GetProcessId() {
|
||||
}
|
||||
|
||||
struct BaseConnectionWin : public BaseConnection {
|
||||
HANDLE pipe { INVALID_HANDLE_VALUE };
|
||||
HANDLE pipe{ INVALID_HANDLE_VALUE };
|
||||
};
|
||||
|
||||
static BaseConnectionWin Connection;
|
||||
@@ -57,10 +57,10 @@ void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
|
||||
bool BaseConnection::Open() {
|
||||
|
||||
wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" };
|
||||
wchar_t pipeName[]{ L"\\\\?\\pipe\\discord-ipc-0" };
|
||||
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
||||
pipeName[pipeDigit] = L'0';
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
for (;;) {
|
||||
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (self->pipe != INVALID_HANDLE_VALUE) {
|
||||
@@ -88,7 +88,7 @@ bool BaseConnection::Open() {
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
::CloseHandle(self->pipe);
|
||||
self->pipe = INVALID_HANDLE_VALUE;
|
||||
self->isOpen = false;
|
||||
@@ -102,7 +102,7 @@ bool BaseConnection::Write(const void *data, size_t length) {
|
||||
if (length == 0) {
|
||||
return true;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
@@ -127,7 +127,7 @@ bool BaseConnection::Read(void *data, size_t length) {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
||||
assert(self);
|
||||
if (!self) {
|
||||
return false;
|
||||
|
||||
6
3rdparty/discord-rpc/discord_msg_queue.h
vendored
6
3rdparty/discord-rpc/discord_msg_queue.h
vendored
@@ -34,9 +34,9 @@ namespace discord_rpc {
|
||||
template<typename ElementType, std::size_t QueueSize>
|
||||
class MsgQueue {
|
||||
ElementType queue_[QueueSize];
|
||||
std::atomic_uint nextAdd_ { 0 };
|
||||
std::atomic_uint nextSend_ { 0 };
|
||||
std::atomic_uint pendingSends_ { 0 };
|
||||
std::atomic_uint nextAdd_{ 0 };
|
||||
std::atomic_uint nextSend_{ 0 };
|
||||
std::atomic_uint pendingSends_{ 0 };
|
||||
|
||||
public:
|
||||
MsgQueue() {}
|
||||
|
||||
@@ -163,4 +163,3 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
|
||||
Discord_RegisterW(appId, wcommand);
|
||||
|
||||
}
|
||||
|
||||
|
||||
68
3rdparty/discord-rpc/discord_rpc.cpp
vendored
68
3rdparty/discord-rpc/discord_rpc.cpp
vendored
@@ -40,9 +40,9 @@ static void Discord_UpdateConnection();
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MaxMessageSize { 16 * 1024 };
|
||||
constexpr size_t MessageQueueSize { 8 };
|
||||
constexpr size_t JoinQueueSize { 8 };
|
||||
constexpr size_t MaxMessageSize{ 16 * 1024 };
|
||||
constexpr size_t MessageQueueSize{ 8 };
|
||||
constexpr size_t JoinQueueSize{ 8 };
|
||||
|
||||
struct QueuedMessage {
|
||||
size_t length;
|
||||
@@ -70,24 +70,24 @@ struct User {
|
||||
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
|
||||
};
|
||||
|
||||
static RpcConnection *Connection { nullptr };
|
||||
static DiscordEventHandlers QueuedHandlers {};
|
||||
static DiscordEventHandlers Handlers {};
|
||||
static std::atomic_bool WasJustConnected { false };
|
||||
static std::atomic_bool WasJustDisconnected { false };
|
||||
static std::atomic_bool GotErrorMessage { false };
|
||||
static std::atomic_bool WasJoinGame { false };
|
||||
static std::atomic_bool WasSpectateGame { false };
|
||||
static std::atomic_bool UpdatePresence { false };
|
||||
static RpcConnection *Connection{ nullptr };
|
||||
static DiscordEventHandlers QueuedHandlers{};
|
||||
static DiscordEventHandlers Handlers{};
|
||||
static std::atomic_bool WasJustConnected{ false };
|
||||
static std::atomic_bool WasJustDisconnected{ false };
|
||||
static std::atomic_bool GotErrorMessage{ false };
|
||||
static std::atomic_bool WasJoinGame{ false };
|
||||
static std::atomic_bool WasSpectateGame{ false };
|
||||
static std::atomic_bool UpdatePresence{ false };
|
||||
static char JoinGameSecret[256];
|
||||
static char SpectateGameSecret[256];
|
||||
static int LastErrorCode { 0 };
|
||||
static int LastErrorCode{ 0 };
|
||||
static char LastErrorMessage[256];
|
||||
static int LastDisconnectErrorCode { 0 };
|
||||
static int LastDisconnectErrorCode{ 0 };
|
||||
static char LastDisconnectErrorMessage[256];
|
||||
static std::mutex PresenceMutex;
|
||||
static std::mutex HandlerMutex;
|
||||
static QueuedMessage QueuedPresence {};
|
||||
static QueuedMessage QueuedPresence{};
|
||||
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
||||
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
||||
static User connectedUser;
|
||||
@@ -95,12 +95,12 @@ static User connectedUser;
|
||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
|
||||
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
||||
static auto NextConnect = std::chrono::system_clock::now();
|
||||
static int Pid { 0 };
|
||||
static int Nonce { 1 };
|
||||
static int Pid{ 0 };
|
||||
static int Nonce{ 1 };
|
||||
|
||||
class IoThreadHolder {
|
||||
private:
|
||||
std::atomic_bool keepRunning { true };
|
||||
std::atomic_bool keepRunning{ true };
|
||||
std::mutex waitForIOMutex;
|
||||
std::condition_variable waitForIOActivity;
|
||||
std::thread ioThread;
|
||||
@@ -109,14 +109,14 @@ class IoThreadHolder {
|
||||
void Start() {
|
||||
keepRunning.store(true);
|
||||
ioThread = std::thread([&]() {
|
||||
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
|
||||
Discord_UpdateConnection();
|
||||
while (keepRunning.load()) {
|
||||
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
||||
waitForIOActivity.wait_for(lock, maxWait);
|
||||
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
|
||||
Discord_UpdateConnection();
|
||||
}
|
||||
});
|
||||
while (keepRunning.load()) {
|
||||
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
||||
waitForIOActivity.wait_for(lock, maxWait);
|
||||
Discord_UpdateConnection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Notify() { waitForIOActivity.notify_all(); }
|
||||
@@ -132,7 +132,7 @@ class IoThreadHolder {
|
||||
~IoThreadHolder() { Stop(); }
|
||||
};
|
||||
|
||||
static IoThreadHolder *IoThread { nullptr };
|
||||
static IoThreadHolder *IoThread{ nullptr };
|
||||
|
||||
static void UpdateReconnectTime() {
|
||||
|
||||
@@ -429,7 +429,7 @@ extern "C" void Discord_RunCallbacks() {
|
||||
if (WasJustConnected.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.ready) {
|
||||
DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||
DiscordUser du{ connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||
Handlers.ready(&du);
|
||||
}
|
||||
}
|
||||
@@ -465,7 +465,7 @@ extern "C" void Discord_RunCallbacks() {
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinRequest) {
|
||||
DiscordUser du { req->userId, req->username, req->discriminator, req->avatar };
|
||||
DiscordUser du{ req->userId, req->username, req->discriminator, req->avatar };
|
||||
Handlers.joinRequest(&du);
|
||||
}
|
||||
}
|
||||
@@ -486,12 +486,12 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
||||
|
||||
if (newHandlers) {
|
||||
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||
RegisterForEvent(event); \
|
||||
} \
|
||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
||||
DeregisterForEvent(event); \
|
||||
}
|
||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||
RegisterForEvent(event); \
|
||||
} \
|
||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
||||
DeregisterForEvent(event); \
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
|
||||
|
||||
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; }
|
||||
|
||||
|
||||
63
Brewfile
Normal file
63
Brewfile
Normal file
@@ -0,0 +1,63 @@
|
||||
# Strawberry Music Player (macOS) - Homebrew Bundle
|
||||
#
|
||||
# Usage:
|
||||
# brew bundle --file Brewfile
|
||||
#
|
||||
# Notes:
|
||||
# - This is intended for macOS (Apple Silicon or Intel).
|
||||
# - Some Strawberry features are optional and will auto-disable if deps are missing.
|
||||
|
||||
# Build tooling
|
||||
brew "cmake"
|
||||
brew "pkg-config"
|
||||
brew "ninja"
|
||||
|
||||
# Optional (developer): unit tests
|
||||
brew "googletest"
|
||||
|
||||
# Core runtime/build dependencies (required by CMakeLists.txt)
|
||||
brew "qt" # Qt 6 (Core/Gui/Widgets/Network/Sql/Concurrent)
|
||||
brew "vulkan-headers" # helps Qt6Gui's WrapVulkanHeaders dependency on some setups
|
||||
brew "boost"
|
||||
brew "icu4c"
|
||||
brew "glib" # provides glib-2.0 + gobject-2.0 (via pkg-config)
|
||||
brew "glib-networking" # TLS + GIO modules (helps macOS bundling via dist/macos/macgstcopy.sh)
|
||||
brew "sqlite"
|
||||
brew "taglib"
|
||||
brew "gstreamer"
|
||||
|
||||
# Strawberry requires KDAB's KDSingleApplication (CMake package name: KDSingleApplication-qt6).
|
||||
# Homebrew core doesn't consistently provide it, so this repo includes a local formula.
|
||||
# Homebrew requires formulae to be installed from a tap; `brew bundle` will tap *this repo*
|
||||
# using the current working directory (run `brew bundle` from the repo root).
|
||||
# If you previously tapped `strawberry/local` before `Formula/` existed, refresh it with:
|
||||
# brew untap strawberry/local && brew tap strawberry/local "file://$PWD"
|
||||
tap "strawberry/local", "file://#{Dir.pwd}"
|
||||
brew "strawberry/local/kdsingleapplication-qt6"
|
||||
brew "strawberry/local/qtsparkle-qt6" # optional: QtSparkle integration
|
||||
brew "strawberry/local/sparkle-framework" # optional: Sparkle integration (framework)
|
||||
brew "strawberry/local/macdeploycheck" # optional: enables CMake target 'deploycheck' (sanity checks deployed .app)
|
||||
|
||||
# Recommended GStreamer plugin sets for broad codec support (matches README guidance)
|
||||
brew "gst-plugins-base"
|
||||
brew "gst-plugins-good"
|
||||
brew "gst-plugins-bad"
|
||||
brew "gst-plugins-ugly"
|
||||
brew "gst-libav"
|
||||
|
||||
# Optional features (silences CMake warnings / enables extra functionality)
|
||||
brew "rapidjson" # enables Discord Rich Presence (DISCORD_RPC)
|
||||
brew "google-sparsehash" # enables stream tagreader (STREAMTAGREADER / libsparsehash)
|
||||
brew "chromaprint" # enables MusicBrainz + song fingerprinting
|
||||
brew "fftw" # enables Moodbar (fftw3)
|
||||
brew "libebur128" # enables EBU R 128 loudness normalization
|
||||
brew "libcdio" # enables Audio CD support
|
||||
brew "libmtp" # enables MTP device support
|
||||
brew "strawberry/local/libgpod" # enables iPod classic support (Homebrew core doesn't provide libgpod)
|
||||
|
||||
# Helpful for Strawberry's macOS "deploy" target (GStreamer dynamically loads libsoup)
|
||||
brew "libsoup"
|
||||
|
||||
# Optional: enable building the CMake "dmg" target (cmake/Dmg.cmake)
|
||||
brew "create-dmg"
|
||||
|
||||
@@ -32,6 +32,18 @@ if(LINUX)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
# Find Sparkle early so cmake/Dmg.cmake (deploy target) can bundle it into the app.
|
||||
# Sparkle is optional; if not found, update functionality is disabled.
|
||||
find_library(SPARKLE Sparkle
|
||||
PATHS
|
||||
/Library/Frameworks
|
||||
/System/Library/Frameworks
|
||||
/opt/homebrew/Frameworks
|
||||
/opt/homebrew/opt/sparkle-framework/Frameworks
|
||||
/usr/local/Frameworks
|
||||
/usr/local/opt/sparkle-framework/Frameworks
|
||||
PATH_SUFFIXES Frameworks
|
||||
)
|
||||
include(cmake/Dmg.cmake)
|
||||
endif()
|
||||
|
||||
@@ -84,8 +96,6 @@ if(MSVC)
|
||||
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
|
||||
else()
|
||||
list(APPEND COMPILE_OPTIONS
|
||||
$<$<COMPILE_LANGUAGE:C>:-std=c11>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wpedantic
|
||||
@@ -253,11 +263,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 +271,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 +706,7 @@ set(SOURCES
|
||||
src/lyrics/elyricsnetlyricsprovider.cpp
|
||||
src/lyrics/letraslyricsprovider.cpp
|
||||
src/lyrics/lyricfindlyricsprovider.cpp
|
||||
src/lyrics/lrcliblyricsprovider.cpp
|
||||
|
||||
src/settings/settingsdialog.cpp
|
||||
src/settings/settingspage.cpp
|
||||
@@ -820,6 +828,8 @@ set(SOURCES
|
||||
|
||||
src/fileview/fileview.cpp
|
||||
src/fileview/fileviewlist.cpp
|
||||
src/fileview/fileviewtree.cpp
|
||||
src/fileview/fileviewtreemodel.cpp
|
||||
|
||||
src/device/devicemanager.cpp
|
||||
src/device/devicelister.cpp
|
||||
@@ -995,6 +1005,7 @@ set(HEADERS
|
||||
src/lyrics/elyricsnetlyricsprovider.h
|
||||
src/lyrics/letraslyricsprovider.h
|
||||
src/lyrics/lyricfindlyricsprovider.h
|
||||
src/lyrics/lrcliblyricsprovider.h
|
||||
|
||||
src/settings/settingsdialog.h
|
||||
src/settings/settingspage.h
|
||||
@@ -1108,6 +1119,8 @@ set(HEADERS
|
||||
|
||||
src/fileview/fileview.h
|
||||
src/fileview/fileviewlist.h
|
||||
src/fileview/fileviewtree.h
|
||||
src/fileview/fileviewtreemodel.h
|
||||
|
||||
src/device/devicemanager.h
|
||||
src/device/devicelister.h
|
||||
@@ -1210,6 +1223,10 @@ set(UI
|
||||
src/device/deviceviewcontainer.ui
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
optional_source(UNIX SOURCES src/core/unixsignalwatcher.cpp HEADERS src/core/unixsignalwatcher.h)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
optional_source(APPLE
|
||||
SOURCES
|
||||
@@ -1438,6 +1455,7 @@ optional_source(HAVE_SPOTIFY
|
||||
src/spotify/spotifybaserequest.cpp
|
||||
src/spotify/spotifyrequest.cpp
|
||||
src/spotify/spotifyfavoriterequest.cpp
|
||||
src/spotify/spotifymetadatarequest.cpp
|
||||
src/settings/spotifysettingspage.cpp
|
||||
src/covermanager/spotifycoverprovider.cpp
|
||||
HEADERS
|
||||
@@ -1445,6 +1463,7 @@ optional_source(HAVE_SPOTIFY
|
||||
src/spotify/spotifybaserequest.h
|
||||
src/spotify/spotifyrequest.h
|
||||
src/spotify/spotifyfavoriterequest.h
|
||||
src/spotify/spotifymetadatarequest.h
|
||||
src/settings/spotifysettingspage.h
|
||||
src/covermanager/spotifycoverprovider.h
|
||||
UI
|
||||
@@ -1459,6 +1478,8 @@ optional_source(HAVE_QOBUZ
|
||||
src/qobuz/qobuzrequest.cpp
|
||||
src/qobuz/qobuzstreamurlrequest.cpp
|
||||
src/qobuz/qobuzfavoriterequest.cpp
|
||||
src/qobuz/qobuzmetadatarequest.cpp
|
||||
src/qobuz/qobuzcredentialfetcher.cpp
|
||||
src/settings/qobuzsettingspage.cpp
|
||||
src/covermanager/qobuzcoverprovider.cpp
|
||||
HEADERS
|
||||
@@ -1468,6 +1489,8 @@ optional_source(HAVE_QOBUZ
|
||||
src/qobuz/qobuzrequest.h
|
||||
src/qobuz/qobuzstreamurlrequest.h
|
||||
src/qobuz/qobuzfavoriterequest.h
|
||||
src/qobuz/qobuzmetadatarequest.h
|
||||
src/qobuz/qobuzcredentialfetcher.h
|
||||
src/settings/qobuzsettingspage.h
|
||||
src/covermanager/qobuzcoverprovider.h
|
||||
UI
|
||||
@@ -1503,10 +1526,22 @@ if(HAVE_DISCORD_RPC)
|
||||
endif()
|
||||
|
||||
if(HAVE_TRANSLATIONS)
|
||||
option(TRANSLATIONS_VERBOSE "Show verbose output while generating .qm translation files" OFF)
|
||||
# On non-Windows platforms Qt doesn't need a PATH-setup wrapper for tools, but we can
|
||||
# provide a wrapper to filter non-actionable lrelease noise during normal builds.
|
||||
if(NOT CMAKE_HOST_WIN32)
|
||||
set(QT_TOOL_COMMAND_WRAPPER_PATH "${CMAKE_SOURCE_DIR}/cmake/qt_tool_wrapper.sh"
|
||||
CACHE INTERNAL "Wrapper used when invoking Qt tools from CMake" FORCE
|
||||
)
|
||||
endif()
|
||||
qt_add_lupdate(strawberry_lib TS_FILES "${CMAKE_SOURCE_DIR}/src/translations/strawberry_en_US.ts" OPTIONS -locations none -no-ui-lines -no-obsolete)
|
||||
file(GLOB_RECURSE ts_files ${CMAKE_SOURCE_DIR}/src/translations/*.ts)
|
||||
set_source_files_properties(${ts_files} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_BINARY_DIR}/data")
|
||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES)
|
||||
if(TRANSLATIONS_VERBOSE)
|
||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES)
|
||||
else()
|
||||
qt_add_lrelease(strawberry TS_FILES ${ts_files} QM_FILES_OUTPUT_VARIABLE INSTALL_TRANSLATIONS_FILES OPTIONS -silent)
|
||||
endif()
|
||||
if(NOT INSTALL_TRANSLATIONS)
|
||||
qt_add_resources(strawberry "translations" PREFIX "/i18n" BASE "${CMAKE_CURRENT_BINARY_DIR}/data" FILES "${INSTALL_TRANSLATIONS_FILES}")
|
||||
endif()
|
||||
|
||||
61
Changelog
61
Changelog
@@ -2,6 +2,65 @@ 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:
|
||||
@@ -284,7 +343,7 @@ Version 1.1.0 (2024.07.14):
|
||||
* Only use playbin3 with GStreamer 1.24 and higher, not with GStreamer 1.22 or lower.
|
||||
* (macOS/Windows) Fixed dash and hls streaming, plugins were missing.
|
||||
* (Windows) Fixed incorrect colors in smart playlist wizard with Fusion in dark mode (#1399).
|
||||
* (Windows) Fixed update window blocking sponsor window on startup.
|
||||
* (Windows) Fixed update window blocking startup window on launch.
|
||||
|
||||
Enhancements:
|
||||
* Improve error messages when connecting and copying to devices.
|
||||
|
||||
51
Formula/kdsingleapplication-qt6.rb
Normal file
51
Formula/kdsingleapplication-qt6.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
class KdsingleapplicationQt6 < Formula
|
||||
desc "Helper class for single-instance Qt applications (Qt 6 build)"
|
||||
homepage "https://github.com/KDAB/KDSingleApplication"
|
||||
url "https://github.com/KDAB/KDSingleApplication/archive/refs/tags/v1.1.0.tar.gz"
|
||||
sha256 "1f19124c0aa5c6fffee3da174f7d2e091fab6dca1e123da70bb0fe615bfbe3e8"
|
||||
license "MIT"
|
||||
|
||||
depends_on "cmake" => :build
|
||||
depends_on "ninja" => :build
|
||||
depends_on "qt"
|
||||
|
||||
def install
|
||||
args = std_cmake_args + %W[
|
||||
-GNinja
|
||||
-DKDSingleApplication_QT6=ON
|
||||
-DKDSingleApplication_TESTS=OFF
|
||||
-DKDSingleApplication_EXAMPLES=OFF
|
||||
-DKDSingleApplication_DOCS=OFF
|
||||
-DKDSingleApplication_DEVELOPER_MODE=OFF
|
||||
-DKDSingleApplication_STATIC=OFF
|
||||
]
|
||||
|
||||
system "cmake", "-S", ".", "-B", "build", *args
|
||||
system "cmake", "--build", "build"
|
||||
system "cmake", "--install", "build"
|
||||
end
|
||||
|
||||
test do
|
||||
# Verify CMake package is usable via find_package(KDSingleApplication-qt6 CONFIG REQUIRED)
|
||||
(testpath/"CMakeLists.txt").write <<~CMAKE
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(kdsa_test LANGUAGES CXX)
|
||||
find_package(KDSingleApplication-qt6 CONFIG REQUIRED)
|
||||
add_executable(test_kdsa main.cpp)
|
||||
target_link_libraries(test_kdsa PRIVATE KDAB::kdsingleapplication)
|
||||
CMAKE
|
||||
|
||||
(testpath/"main.cpp").write <<~CPP
|
||||
#include <QCoreApplication>
|
||||
int main(int argc, char** argv) {
|
||||
QCoreApplication app(argc, argv);
|
||||
return 0;
|
||||
}
|
||||
CPP
|
||||
|
||||
system "cmake", "-S", ".", "-B", "build",
|
||||
"-DCMAKE_PREFIX_PATH=#{opt_prefix}"
|
||||
system "cmake", "--build", "build"
|
||||
end
|
||||
end
|
||||
|
||||
25
Formula/kdsingleapplication-qt6/README.md
Normal file
25
Formula/kdsingleapplication-qt6/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# kdsingleapplication-qt6 (local Homebrew formula)
|
||||
|
||||
This directory exists to keep any supporting files for the local Homebrew formula
|
||||
next to it (e.g. patches or notes).
|
||||
|
||||
## Install (from this Strawberry repo)
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
brew tap strawberry/local "file://$PWD"
|
||||
brew install strawberry/local/kdsingleapplication-qt6
|
||||
```
|
||||
|
||||
## Why it exists
|
||||
|
||||
Strawberry’s build requires the CMake package `KDSingleApplication-qt6`, but it is
|
||||
not consistently available via Homebrew core. Shipping a local formula makes the
|
||||
dependency easy to install for anyone building this repo on macOS.
|
||||
|
||||
## Note for local development
|
||||
|
||||
Homebrew taps are Git clones, so the formula must be committed (or pushed to a remote)
|
||||
to be visible to `brew tap`.
|
||||
|
||||
81
Formula/libgpod.rb
Normal file
81
Formula/libgpod.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
class Libgpod < Formula
|
||||
desc "Library to access the contents of classic iPods"
|
||||
homepage "https://gtkpod.org/libgpod/"
|
||||
url "https://github.com/neuschaefer/libgpod/archive/0dda196286f5e42be89f0b870abd9278213989a5.tar.gz"
|
||||
sha256 "a9809f85b2b763196ac7c94903211a927efd37a24ef39c355c21b4a1bed28e52"
|
||||
license "LGPL-2.0-only"
|
||||
|
||||
depends_on "autoconf" => :build
|
||||
depends_on "automake" => :build
|
||||
depends_on "libtool" => :build
|
||||
depends_on "pkg-config" => :build
|
||||
depends_on "gtk-doc" => :build
|
||||
depends_on "intltool" => :build
|
||||
|
||||
depends_on "glib"
|
||||
depends_on "gdk-pixbuf"
|
||||
depends_on "libplist"
|
||||
depends_on "libxml2"
|
||||
depends_on "sqlite"
|
||||
|
||||
def install
|
||||
# libgpod's configure.ac checks for pkg-config module name "libplist".
|
||||
# Homebrew provides "libplist-2.0", so we provide a tiny shim .pc file to
|
||||
# satisfy the expected name.
|
||||
(buildpath/"brew-pkgconfig").mkpath
|
||||
(buildpath/"brew-pkgconfig/libplist.pc").write <<~EOS
|
||||
prefix=#{Formula["libplist"].opt_prefix}
|
||||
exec_prefix=${prefix}
|
||||
libdir=#{Formula["libplist"].opt_lib}
|
||||
includedir=#{Formula["libplist"].opt_include}
|
||||
|
||||
Name: libplist
|
||||
Description: Apple property list library (Homebrew shim for libgpod)
|
||||
Version: #{Formula["libplist"].version}
|
||||
Libs: -L${libdir} -lplist-2.0
|
||||
Cflags: -I${includedir}
|
||||
EOS
|
||||
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", buildpath/"brew-pkgconfig"
|
||||
|
||||
# Ensure pkg-config can find Homebrew keg .pc files during configure.
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["libplist"].opt_lib/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["sqlite"].opt_lib/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["glib"].opt_lib/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["glib"].opt_share/"pkgconfig"
|
||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["gdk-pixbuf"].opt_lib/"pkgconfig"
|
||||
|
||||
# Upstream's autogen.sh is very old and may hardcode ancient automake checks
|
||||
# (e.g. looking for automake-1.7). Using autoreconf is the standard Homebrew
|
||||
# way and works with modern autotools.
|
||||
#
|
||||
# libgpod's build system expects gtk-doc's makefile snippet to exist (gtk-doc.make),
|
||||
# which is normally provided by running gtkdocize.
|
||||
system "gtkdocize", "--copy"
|
||||
|
||||
# libgpod also uses intltool's Autoconf macros (IT_PROG_INTLTOOL). If intltoolize
|
||||
# is not run, the generated ./configure may contain unexpanded macros and fail.
|
||||
system "intltoolize", "--force", "--copy", "--automake"
|
||||
system "autoreconf", "-fiv"
|
||||
|
||||
system "./configure", *std_configure_args,
|
||||
"--disable-dependency-tracking",
|
||||
"--with-hal=no",
|
||||
"--disable-udev",
|
||||
"--without-libimobiledevice",
|
||||
"--with-python=no",
|
||||
"--with-mono=no",
|
||||
"--disable-gtk-doc",
|
||||
"--disable-gtk-doc-html",
|
||||
"--disable-gtk-doc-pdf",
|
||||
"--enable-more-warnings=no"
|
||||
|
||||
system "make", "install"
|
||||
end
|
||||
|
||||
test do
|
||||
# Ensure pkg-config can find the expected module name used by Strawberry.
|
||||
assert_match "libgpod", shell_output("pkg-config --libs libgpod-1.0")
|
||||
end
|
||||
end
|
||||
|
||||
8
Formula/libgpod/README.md
Normal file
8
Formula/libgpod/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# libgpod (local Homebrew formula)
|
||||
|
||||
Homebrew core does not currently ship `libgpod`, but Strawberry can optionally use it
|
||||
to support **classic iPod** devices (via `libgpod-1.0` + `gdk-pixbuf-2.0`).
|
||||
|
||||
This formula is pinned to a known-good upstream snapshot and disables Linux-specific
|
||||
integration (udev/HAL) and language bindings to keep the build reliable on macOS.
|
||||
|
||||
93
Formula/macdeploycheck.rb
Normal file
93
Formula/macdeploycheck.rb
Normal file
@@ -0,0 +1,93 @@
|
||||
class Macdeploycheck < Formula
|
||||
desc "Sanity checks a macOS .app bundle for accidental Homebrew runtime dependencies"
|
||||
homepage "https://github.com/strawberrymusicplayer/strawberry"
|
||||
url "file://#{__FILE__}"
|
||||
version "0.1.0"
|
||||
sha256 :no_check
|
||||
license "MIT"
|
||||
|
||||
depends_on :macos
|
||||
|
||||
def install
|
||||
(bin/"macdeploycheck").write <<~'EOS'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
app="${1:-}"
|
||||
if [[ -z "$app" ]]; then
|
||||
echo "Usage: macdeploycheck <path/to/App.app>" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "$app" ]]; then
|
||||
echo "Error: app bundle not found: $app" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "$app/Contents" ]]; then
|
||||
echo "Error: not a macOS app bundle (missing Contents/): $app" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
fail=0
|
||||
tmp="$(mktemp -t macdeploycheck.XXXXXX)"
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
|
||||
# Collect Mach-O files (executables + dylibs) inside the bundle.
|
||||
while IFS= read -r -d '' f; do
|
||||
if file "$f" | grep -q "Mach-O"; then
|
||||
echo "$f" >>"$tmp"
|
||||
fi
|
||||
done < <(find "$app/Contents" -type f \( -perm -111 -o -name "*.dylib" -o -name "*.so" \) -print0 2>/dev/null)
|
||||
|
||||
if [[ ! -s "$tmp" ]]; then
|
||||
echo "Warning: no Mach-O files found under $app/Contents" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "macdeploycheck: scanning for external (Homebrew) runtime deps..."
|
||||
while IFS= read -r f; do
|
||||
# otool -L prints:
|
||||
# <file>:
|
||||
# <dep> (compatibility version ..., current version ...)
|
||||
deps="$(otool -L "$f" 2>/dev/null | tail -n +2 | awk '{print $1}' || true)"
|
||||
while IFS= read -r dep; do
|
||||
[[ -z "$dep" ]] && continue
|
||||
|
||||
# Ignore system and rpath/loader/executable paths.
|
||||
case "$dep" in
|
||||
/System/*|/usr/lib/*|@rpath/*|@loader_path/*|@executable_path/*) continue ;;
|
||||
esac
|
||||
|
||||
# These are the common accidental runtime deps that will break distribution.
|
||||
if [[ "$dep" == /opt/homebrew/* || "$dep" == /usr/local/* || "$dep" == /opt/local/* ]]; then
|
||||
echo "ERROR: $f links to external path: $dep" >&2
|
||||
fail=1
|
||||
fi
|
||||
done <<<"$deps"
|
||||
done <"$tmp"
|
||||
|
||||
if [[ "$fail" -ne 0 ]]; then
|
||||
cat >&2 <<'EOM'
|
||||
|
||||
One or more binaries in your .app link to a Homebrew (or MacPorts) path.
|
||||
That usually means the bundle is not self-contained and will fail on other machines,
|
||||
or will fail notarization/codesigning validation.
|
||||
|
||||
Fix: re-run your deploy step (e.g. macdeployqt) so frameworks/dylibs are bundled and
|
||||
their install names are rewritten to @rpath/@loader_path.
|
||||
EOM
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: no external Homebrew/MacPorts runtime deps detected."
|
||||
exit 0
|
||||
EOS
|
||||
|
||||
chmod 0755, bin/"macdeploycheck"
|
||||
end
|
||||
|
||||
test do
|
||||
# Basic smoke test: tool runs and prints usage.
|
||||
system bin/"macdeploycheck"
|
||||
end
|
||||
end
|
||||
|
||||
36
Formula/macdeploycheck/README.md
Normal file
36
Formula/macdeploycheck/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# `macdeploycheck` (local Homebrew formula)
|
||||
|
||||
This repository includes a small helper tool called `macdeploycheck`, packaged as a local Homebrew formula.
|
||||
|
||||
## What it does
|
||||
|
||||
`macdeploycheck` scans a built `.app` bundle and flags common **accidental runtime dependencies** on:
|
||||
|
||||
- Homebrew paths like `/opt/homebrew/...` or `/usr/local/...`
|
||||
- MacPorts paths like `/opt/local/...`
|
||||
|
||||
These dependencies usually mean the `.app` is **not self-contained** and may fail to run on other machines or fail notarization validation.
|
||||
|
||||
## Install (via this repo's tap)
|
||||
|
||||
From the Strawberry repo root:
|
||||
|
||||
```bash
|
||||
brew tap strawberry/local "file://$PWD"
|
||||
brew install strawberry/local/macdeploycheck
|
||||
```
|
||||
|
||||
Or use the repo `Brewfile`:
|
||||
|
||||
```bash
|
||||
brew bundle --file Brewfile
|
||||
```
|
||||
|
||||
## Use
|
||||
|
||||
```bash
|
||||
macdeploycheck /path/to/Strawberry.app
|
||||
```
|
||||
|
||||
It exits non-zero if it finds external runtime deps.
|
||||
|
||||
45
Formula/qtsparkle-qt6.rb
Normal file
45
Formula/qtsparkle-qt6.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
class QtsparkleQt6 < Formula
|
||||
desc "Qt wrapper library for in-app updates (Qt 6 build)"
|
||||
homepage "https://github.com/strawberrymusicplayer/qtsparkle"
|
||||
url "https://github.com/strawberrymusicplayer/qtsparkle/archive/95ca3b77a79540d632b29e9a4df9aed30af5f901.tar.gz"
|
||||
sha256 "945c9e96d2f6175b134a8ccfd6ec1acd268266d31969b5870d4037e8e5877834"
|
||||
license "GPL-3.0-or-later"
|
||||
|
||||
depends_on "cmake" => :build
|
||||
depends_on "ninja" => :build
|
||||
depends_on "qt"
|
||||
|
||||
def install
|
||||
args = std_cmake_args + %W[
|
||||
-GNinja
|
||||
-DBUILD_WITH_QT6=ON
|
||||
-DBUILD_WITH_QT5=OFF
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
-DBUILD_STATIC_LIBS=OFF
|
||||
]
|
||||
|
||||
system "cmake", "-S", ".", "-B", "build", *args
|
||||
system "cmake", "--build", "build"
|
||||
system "cmake", "--install", "build"
|
||||
end
|
||||
|
||||
test do
|
||||
# Strawberry expects: find_package(qtsparkle-qt6) and target qtsparkle-qt6::qtsparkle
|
||||
(testpath/"CMakeLists.txt").write <<~CMAKE
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(qtsparkle_test LANGUAGES CXX)
|
||||
find_package(qtsparkle-qt6 CONFIG REQUIRED)
|
||||
add_library(dummy STATIC dummy.cpp)
|
||||
target_link_libraries(dummy PRIVATE qtsparkle-qt6::qtsparkle)
|
||||
CMAKE
|
||||
|
||||
(testpath/"dummy.cpp").write <<~CPP
|
||||
int dummy() { return 0; }
|
||||
CPP
|
||||
|
||||
system "cmake", "-S", ".", "-B", "build",
|
||||
"-DCMAKE_PREFIX_PATH=#{opt_prefix}"
|
||||
system "cmake", "--build", "build"
|
||||
end
|
||||
end
|
||||
|
||||
10
Formula/qtsparkle-qt6/README.md
Normal file
10
Formula/qtsparkle-qt6/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# qtsparkle-qt6 (local Homebrew formula)
|
||||
|
||||
This installs Strawberry’s Qt updater helper library as a CMake package:
|
||||
|
||||
- `find_package(qtsparkle-qt6 CONFIG REQUIRED)`
|
||||
- target: `qtsparkle-qt6::qtsparkle`
|
||||
|
||||
Strawberry will pick it up automatically when present and enable the optional
|
||||
**QtSparkle integration**.
|
||||
|
||||
19
Formula/sparkle-framework.rb
Normal file
19
Formula/sparkle-framework.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
class SparkleFramework < Formula
|
||||
desc "Sparkle.framework for macOS app updates (framework-only packaging)"
|
||||
homepage "https://sparkle-project.org/"
|
||||
url "https://github.com/sparkle-project/Sparkle/releases/download/2.8.1/Sparkle-2.8.1.tar.xz"
|
||||
sha256 "5cddb7695674ef7704268f38eccaee80e3accbf19e61c1689efff5b6116d85be"
|
||||
license "MIT"
|
||||
|
||||
depends_on :macos
|
||||
|
||||
def install
|
||||
frameworks = prefix/"Frameworks"
|
||||
frameworks.install "Sparkle.framework"
|
||||
end
|
||||
|
||||
test do
|
||||
assert_predicate prefix/"Frameworks/Sparkle.framework", :exist?
|
||||
end
|
||||
end
|
||||
|
||||
9
Formula/sparkle-framework/README.md
Normal file
9
Formula/sparkle-framework/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# sparkle-framework (local Homebrew formula)
|
||||
|
||||
Installs the upstream `Sparkle.framework` into:
|
||||
|
||||
- `$(brew --prefix sparkle-framework)/Frameworks/Sparkle.framework`
|
||||
|
||||
This is used to enable Strawberry’s optional **Sparkle integration** on macOS
|
||||
(`find_library(SPARKLE Sparkle)` in the main `CMakeLists.txt`).
|
||||
|
||||
76
README.md
76
README.md
@@ -1,58 +1,49 @@
|
||||
# :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**, originally forked from *Clementine* in 2018.
|
||||
It’s written in **C++ using the Qt framework**, designed for **audiophiles and music collectors**.
|
||||
This repository is a **macOS-focused fork** of the upstream Strawberry Music Player project originally created and maintained by **Jonas Kvinge** (itself originally forked from Clementine).
|
||||
|
||||

|
||||
The goal of this fork is to make Strawberry **build cleanly and repeatably on macOS**, with:
|
||||
|
||||
---
|
||||
- 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
|
||||
|
||||
## :globe_with_meridians: Resources
|
||||
## Upstream vs this fork (macOS distribution)
|
||||
|
||||
- **Website:** https://www.strawberrymusicplayer.org
|
||||
- **Wiki:** https://wiki.strawberrymusicplayer.org
|
||||
- **Forum:** https://forum.strawberrymusicplayer.org
|
||||
- **GitHub:** https://github.com/strawberrymusicplayer/strawberry
|
||||
- **Latest builds:** https://builds.strawberrymusicplayer.org
|
||||
- **openSUSE Build Service:** https://build.opensuse.org/package/show/home:jonaski:audio/strawberry
|
||||
- **Ubuntu PPAs:**
|
||||
- Stable: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||
- Unstable: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
|
||||
- **Translations:** https://crowdin.com/project/strawberrymusicplayer
|
||||
Upstream Strawberry is free/open-source, but upstream’s **macOS binaries** may be distributed via a **paid-access channel** depending on the release period and policy.
|
||||
|
||||
---
|
||||
This fork is intended for people who want to:
|
||||
|
||||
## :warning: Opening an Issue
|
||||
- **build from source on macOS** without guesswork
|
||||
- **produce signed + notarized binaries** themselves (and optionally distribute them)
|
||||
|
||||
Before creating a new GitHub issue:
|
||||
General safety note: whether you use upstream builds, your own builds, or someone else’s, only install software from sources you trust and prefer **signed + notarized** releases.
|
||||
|
||||
1. **Read the [FAQ](https://wiki.strawberrymusicplayer.org/wiki/FAQ)**.
|
||||
2. **Search existing issues** to avoid duplicates. If one already exists, comment there with any additional information.
|
||||
3. **Use the [forum](https://forum.strawberrymusicplayer.org/)** for technical problems, discussions or feature suggestions — it’s better suited for back-and-forth conversation.
|
||||
4. **Feature requests are not accepted on GitHub.** Issues created for feature requests will be closed. You can still discuss ideas on the forum.
|
||||
5. **Flatpak users:** We do **not** maintain the Flatpak package. Report Flatpak-specific issues via [Flatpak support](https://flatpak.org/about/).
|
||||
## Quick start (macOS)
|
||||
|
||||
---
|
||||
Install Homebrew dependencies:
|
||||
|
||||
## :moneybag: Sponsoring
|
||||
```bash
|
||||
./build_tools/macos/install_brew_deps.sh
|
||||
```
|
||||
|
||||
Strawberry is **free software released under the GPL**.
|
||||
If you enjoy using it, please consider **supporting development** through sponsorship or donation.
|
||||
Build:
|
||||
|
||||
**Sponsorship options:**
|
||||
1. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||
2. [GitHub](https://github.com/sponsors/jonaski)
|
||||
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
||||
4. [PayPal](https://paypal.me/jonaskvinge)
|
||||
```bash
|
||||
./build_tools/macos/build_app.sh --release --clean
|
||||
open ./cmake-build-macos-release/strawberry.app
|
||||
```
|
||||
|
||||
Supporting open-source developers helps ensure continued maintenance and improvements.
|
||||
Build + deploy + sign + notarize (+ DMG):
|
||||
|
||||
---
|
||||
```bash
|
||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy --dmg \
|
||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
||||
--notary-profile "<profile-name>"
|
||||
```
|
||||
|
||||
## :white_check_mark: Features
|
||||
## Features
|
||||
|
||||
- Play and organize your music collection
|
||||
- Supports formats: WAV, FLAC, WavPack, Ogg Vorbis, Opus, MPC, TrueAudio, AIFF, MP4, MP3, ASF, and Monkey’s Audio
|
||||
@@ -64,7 +55,7 @@ Supporting open-source developers helps ensure continued maintenance and improve
|
||||
- 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)
|
||||
- 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/)
|
||||
@@ -76,11 +67,6 @@ Supporting open-source developers helps ensure continued maintenance and improve
|
||||
|
||||
:white_check_mark: Tested on **Linux**, **OpenBSD**, **FreeBSD**, **macOS**, and **Windows**.
|
||||
|
||||
> **Note:** macOS and Windows releases are currently **available to sponsors only**.
|
||||
> A monthly sponsorship via [Patreon](https://www.patreon.com/jonaskvinge) grants direct access to new releases.
|
||||
|
||||
---
|
||||
|
||||
## :gear: Requirements
|
||||
|
||||
To build Strawberry from source, you’ll need:
|
||||
|
||||
79
build_tools/README.md
Normal file
79
build_tools/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Build helper scripts
|
||||
|
||||
This `build_tools/` directory contains **helper scripts and notes** for building Strawberry.
|
||||
|
||||
- It is **not** intended to be your CMake build output directory.
|
||||
- Recommended CMake build output directories: `cmake-build/`, `build-release/`, etc.
|
||||
|
||||
## macOS
|
||||
|
||||
- Install dependencies via Homebrew:
|
||||
|
||||
```bash
|
||||
./build_tools/macos/install_brew_deps.sh
|
||||
```
|
||||
|
||||
- Build Strawberry:
|
||||
|
||||
```bash
|
||||
./build_tools/macos/build_app.sh --release
|
||||
open ./cmake-build-macos-release/strawberry.app
|
||||
```
|
||||
|
||||
## macOS signing + notarization (Developer ID distribution)
|
||||
|
||||
This repo includes `build_tools/macos/build_sign_notarize.sh` to automate:
|
||||
|
||||
- build → (optional deploy) → codesign → notarize → staple → verify
|
||||
|
||||
### One-time setup (Apple Developer)
|
||||
|
||||
- **Install certificates**:
|
||||
- In the Apple Developer portal, create (or download) a **Developer ID Application** certificate.
|
||||
- Install it into your login keychain (Xcode can manage this via **Xcode → Settings → Accounts**).
|
||||
|
||||
- **Provisioning profiles**:
|
||||
- For **Developer ID distribution (outside the Mac App Store)**, you typically **do not need a provisioning profile**.
|
||||
- You *do* need profiles if you are building a **Mac App Store**-signed app (not what this repo’s scripts target).
|
||||
|
||||
- **Notarization credentials**:
|
||||
- Create a `notarytool` keychain profile (recommended) so you don’t have to pass secrets on the command line:
|
||||
|
||||
```bash
|
||||
# NOTE: <profile-name> is a positional argument (not a flag).
|
||||
# Pick any name you want, e.g. "strawberry-notary".
|
||||
xcrun notarytool store-credentials "<profile-name>" \
|
||||
--apple-id "<your-apple-id>" \
|
||||
--team-id "<TEAMID>" \
|
||||
--password "<app-specific-password>"
|
||||
```
|
||||
|
||||
### Listing what’s installed locally
|
||||
|
||||
Run with no args to list local signing identities + notarytool profiles:
|
||||
|
||||
```bash
|
||||
./build_tools/macos/build_sign_notarize.sh
|
||||
```
|
||||
|
||||
### Build + sign + notarize
|
||||
|
||||
```bash
|
||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy \
|
||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
||||
--notary-profile "<profile-name>"
|
||||
```
|
||||
|
||||
### Build + sign + notarize + DMG (recommended for public distribution)
|
||||
|
||||
This produces:
|
||||
|
||||
- a notarized `strawberry.app` (stapled)
|
||||
- a notarized `strawberry-notarize.zip` (useful for Sparkle / downloads)
|
||||
- a notarized `strawberry-*.dmg` (stapled)
|
||||
|
||||
```bash
|
||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy --dmg \
|
||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
||||
--notary-profile "<profile-name>"
|
||||
```
|
||||
266
build_tools/macos/build_app.sh
Executable file
266
build_tools/macos/build_app.sh
Executable file
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ts() { date +"%H:%M:%S"; }
|
||||
lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; }
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
||||
|
||||
current_cmd_pid=""
|
||||
current_hb_pid=""
|
||||
|
||||
kill_tree() {
|
||||
local pid="$1"
|
||||
[[ -z "${pid}" ]] && return 0
|
||||
# Recurse into children first (best-effort).
|
||||
local child
|
||||
for child in $(pgrep -P "$pid" 2>/dev/null || true); do
|
||||
kill_tree "$child"
|
||||
done
|
||||
kill -TERM "$pid" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
# Never fail cleanup on errors.
|
||||
set +e
|
||||
if [[ -n "${current_hb_pid}" ]]; then
|
||||
kill "${current_hb_pid}" >/dev/null 2>&1 || true
|
||||
wait "${current_hb_pid}" >/dev/null 2>&1 || true
|
||||
current_hb_pid=""
|
||||
fi
|
||||
if [[ -n "${current_cmd_pid}" ]]; then
|
||||
# If still running, terminate process tree.
|
||||
kill -0 "${current_cmd_pid}" >/dev/null 2>&1 && kill_tree "${current_cmd_pid}"
|
||||
current_cmd_pid=""
|
||||
fi
|
||||
}
|
||||
|
||||
trap 'cleanup; exit 130' INT TERM
|
||||
trap 'cleanup' EXIT
|
||||
|
||||
run_with_heartbeat() {
|
||||
local desc="$1"
|
||||
shift
|
||||
|
||||
local start now elapsed hb_pid
|
||||
start="$(date +%s)"
|
||||
|
||||
echo "==> [$(ts)] ${desc}"
|
||||
|
||||
# Run the command in the background so we can reliably clean it up on Ctrl-C.
|
||||
set +e
|
||||
"$@" &
|
||||
local cmd_pid=$!
|
||||
set -e
|
||||
current_cmd_pid="$cmd_pid"
|
||||
|
||||
(
|
||||
while kill -0 "$cmd_pid" >/dev/null 2>&1; do
|
||||
sleep 20
|
||||
now="$(date +%s)"
|
||||
elapsed="$((now - start))"
|
||||
echo " [$(ts)] ... still working (${elapsed}s elapsed) ..."
|
||||
done
|
||||
) &
|
||||
hb_pid="$!"
|
||||
current_hb_pid="$hb_pid"
|
||||
|
||||
set +e
|
||||
wait "$cmd_pid"
|
||||
local rc=$?
|
||||
set -e
|
||||
|
||||
# Clear globals before stopping heartbeat to avoid cleanup double-kill.
|
||||
current_cmd_pid=""
|
||||
kill "$hb_pid" >/dev/null 2>&1 || true
|
||||
wait "$hb_pid" >/dev/null 2>&1 || true
|
||||
current_hb_pid=""
|
||||
|
||||
now="$(date +%s)"
|
||||
elapsed="$((now - start))"
|
||||
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
echo "Error: '${desc}' failed after ${elapsed}s (exit $rc)." >&2
|
||||
return "$rc"
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Done: ${desc} (${elapsed}s)"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./build_tools/macos/build_app.sh [--debug|--release] [--deploy] [--dmg] [--clean] [--build-dir <path>]
|
||||
|
||||
What it does:
|
||||
- Configures and builds Strawberry with CMake + Ninja
|
||||
- Optional: runs CMake targets 'deploy' (bundle deps) and 'dmg' (create DMG)
|
||||
|
||||
Options:
|
||||
--release Release build (default)
|
||||
--debug Debug build
|
||||
--deploy Run: cmake --build <builddir> --target deploy
|
||||
--dmg Run: cmake --build <builddir> --target dmg (implies --deploy)
|
||||
--clean Delete the build dir before configuring
|
||||
--build-dir Override build directory (default: <repo>/cmake-build-macos-<config>)
|
||||
-h, --help Show help
|
||||
EOF
|
||||
}
|
||||
|
||||
config="Release"
|
||||
do_deploy=0
|
||||
do_dmg=0
|
||||
do_clean=0
|
||||
build_dir=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--release) config="Release"; shift ;;
|
||||
--debug) config="Debug"; shift ;;
|
||||
--deploy) do_deploy=1; shift ;;
|
||||
--dmg) do_dmg=1; do_deploy=1; shift ;;
|
||||
--clean) do_clean=1; shift ;;
|
||||
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
echo "Error: This script is for macOS only." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then
|
||||
echo "Error: Xcode Command Line Tools not found." >&2
|
||||
echo "Install them first: xcode-select --install" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
echo "Error: Homebrew ('brew') not found in PATH." >&2
|
||||
echo "Install Homebrew first: https://brew.sh/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v cmake >/dev/null 2>&1; then
|
||||
echo "Error: cmake not found. Did you run ./build_tools/macos/install_brew_deps.sh ?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v ninja >/dev/null 2>&1; then
|
||||
echo "Error: ninja not found. Did you run ./build_tools/macos/install_brew_deps.sh ?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
brew_prefix="$(brew --prefix)"
|
||||
qt_prefix="$(brew --prefix qt)"
|
||||
icu_prefix="$(brew --prefix icu4c || true)"
|
||||
|
||||
if [[ -z "$build_dir" ]]; then
|
||||
build_dir="${repo_root}/cmake-build-macos-$(lower "$config")"
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Repo: ${repo_root}"
|
||||
echo "==> [$(ts)] Build dir: ${build_dir}"
|
||||
echo "==> [$(ts)] Config: ${config}"
|
||||
|
||||
if [[ "$do_clean" -eq 1 ]]; then
|
||||
echo "==> [$(ts)] Cleaning build dir"
|
||||
# macOS 26+ can apply provenance metadata that blocks deletion even when permissions look normal.
|
||||
# Clear common xattrs and immutable flags before deleting.
|
||||
xattr -dr com.apple.provenance "$build_dir" >/dev/null 2>&1 || true
|
||||
xattr -dr com.apple.quarantine "$build_dir" >/dev/null 2>&1 || true
|
||||
chflags -R nouchg,noschg "$build_dir" >/dev/null 2>&1 || true
|
||||
rm -rf "$build_dir" || {
|
||||
echo "Error: failed to remove build dir: $build_dir" >&2
|
||||
echo "This is usually due to macOS provenance/flags. Try:" >&2
|
||||
echo " xattr -dr com.apple.provenance \"$build_dir\"" >&2
|
||||
echo " chflags -R nouchg,noschg \"$build_dir\"" >&2
|
||||
echo " rm -rf \"$build_dir\"" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
mkdir -p "$build_dir"
|
||||
|
||||
# If you've run a previously-built app directly from the build directory, macOS can apply provenance
|
||||
# metadata that makes the bundle effectively immutable (even when permissions look normal).
|
||||
# That breaks CMake because it needs to update strawberry.app/Contents/Info.plist during configure/build.
|
||||
app_bundle="${build_dir}/strawberry.app"
|
||||
if [[ -d "${app_bundle}/Contents" ]]; then
|
||||
# Try to clear provenance/quarantine metadata first (best effort).
|
||||
xattr -dr com.apple.provenance "${app_bundle}" >/dev/null 2>&1 || true
|
||||
xattr -dr com.apple.quarantine "${app_bundle}" >/dev/null 2>&1 || true
|
||||
|
||||
# If the bundle is still not writable, remove it so CMake can recreate it.
|
||||
if ! ( : > "${app_bundle}/Contents/.cmake_write_test" ) 2>/dev/null; then
|
||||
echo "==> [$(ts)] Existing ${app_bundle} is not writable (likely macOS provenance). Removing it."
|
||||
rm -rf "${app_bundle}"
|
||||
else
|
||||
rm -f "${app_bundle}/Contents/.cmake_write_test" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make pkg-config more reliable with Homebrew.
|
||||
export PKG_CONFIG_PATH="${brew_prefix}/lib/pkgconfig:${brew_prefix}/share/pkgconfig:${PKG_CONFIG_PATH:-}"
|
||||
|
||||
# For dist/CMakeLists.txt Info.plist minimum version logic.
|
||||
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-12.0}"
|
||||
|
||||
cmake_prefix_path="${qt_prefix};${brew_prefix}"
|
||||
|
||||
cmake_extra_args=()
|
||||
|
||||
# Optional: override Sparkle update feed / key for your own published builds.
|
||||
# Example:
|
||||
# export SPARKLE_FEED_URL="https://example.com/appcast.xml"
|
||||
# export SPARKLE_PUBLIC_ED25519_KEY="base64=="
|
||||
if [[ -n "${SPARKLE_FEED_URL:-}" ]]; then
|
||||
cmake_extra_args+=("-DSPARKLE_FEED_URL=${SPARKLE_FEED_URL}")
|
||||
fi
|
||||
if [[ -n "${SPARKLE_PUBLIC_ED25519_KEY:-}" ]]; then
|
||||
cmake_extra_args+=("-DSPARKLE_PUBLIC_ED25519_KEY=${SPARKLE_PUBLIC_ED25519_KEY}")
|
||||
fi
|
||||
|
||||
run_with_heartbeat "Configuring (CMAKE_PREFIX_PATH=${cmake_prefix_path})" \
|
||||
cmake -S "$repo_root" -B "$build_dir" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE="$config" \
|
||||
-DCMAKE_PREFIX_PATH="$cmake_prefix_path" \
|
||||
-DCMAKE_FRAMEWORK_PATH="${brew_prefix}/Frameworks;${brew_prefix}/opt/sparkle-framework/Frameworks" \
|
||||
-DOPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL=OFF \
|
||||
${cmake_extra_args+"${cmake_extra_args[@]}"} \
|
||||
${icu_prefix:+-DICU_ROOT="$icu_prefix"}
|
||||
|
||||
run_with_heartbeat "Building" \
|
||||
cmake --build "$build_dir" --parallel
|
||||
|
||||
if [[ "$do_deploy" -eq 1 ]]; then
|
||||
echo "==> [$(ts)] Preparing env for 'deploy' target (GIO/GStreamer)"
|
||||
export GIO_EXTRA_MODULES="${brew_prefix}/lib/gio/modules"
|
||||
export GST_PLUGIN_SCANNER="${brew_prefix}/opt/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner"
|
||||
export GST_PLUGIN_PATH="${brew_prefix}/lib/gstreamer-1.0"
|
||||
|
||||
# Optional, but helps dist/macos/macgstcopy.sh bundle libsoup which GStreamer loads dynamically.
|
||||
libsoup_prefix="$(brew --prefix libsoup 2>/dev/null || true)"
|
||||
if [[ -n "${libsoup_prefix}" ]]; then
|
||||
libsoup_dylib="$(ls -1 "${libsoup_prefix}"/lib/libsoup-*.dylib 2>/dev/null | head -n 1 || true)"
|
||||
if [[ -n "${libsoup_dylib}" ]]; then
|
||||
export LIBSOUP_LIBRARY_PATH="${libsoup_dylib}"
|
||||
fi
|
||||
fi
|
||||
|
||||
run_with_heartbeat "Running: deploy" \
|
||||
cmake --build "$build_dir" --target deploy
|
||||
fi
|
||||
|
||||
if [[ "$do_dmg" -eq 1 ]]; then
|
||||
run_with_heartbeat "Running: dmg" \
|
||||
cmake --build "$build_dir" --target dmg
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Done"
|
||||
echo "Built app:"
|
||||
echo " ${build_dir}/strawberry.app"
|
||||
|
||||
309
build_tools/macos/build_sign_notarize.sh
Executable file
309
build_tools/macos/build_sign_notarize.sh
Executable file
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ts() { date +"%H:%M:%S"; }
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./build_tools/macos/build_sign_notarize.sh # list local signing identities + notary profiles
|
||||
./build_tools/macos/build_sign_notarize.sh --run [options] # build, sign, notarize, staple
|
||||
|
||||
Common options:
|
||||
--run Perform build/sign/notarize (otherwise list identities/profiles)
|
||||
--release | --debug Build config (default: Release)
|
||||
--clean Clean build dir before build
|
||||
--deploy Run CMake 'deploy' target before signing (default: on)
|
||||
--no-deploy Do not run 'deploy' (not recommended for distribution)
|
||||
--dmg Build a DMG after app notarization, then notarize+staple the DMG too
|
||||
--build-dir <path> Override build directory
|
||||
|
||||
Signing options:
|
||||
--identity "<name>" Codesign identity (e.g. "Developer ID Application: Your Name (TEAMID)")
|
||||
--entitlements <plist> Optional entitlements plist for codesign
|
||||
|
||||
Notarization options (recommended):
|
||||
--notary-profile <name> notarytool keychain profile name (created via `xcrun notarytool store-credentials <name> ...`)
|
||||
--skip-notarize Skip notarization
|
||||
|
||||
Outputs:
|
||||
- Signed app: <build-dir>/strawberry.app
|
||||
- Zip for notarization: <build-dir>/strawberry-notarize.zip
|
||||
- DMG (optional): <build-dir>/strawberry-*.dmg
|
||||
|
||||
Notes:
|
||||
- This script is intended for Developer ID distribution (outside Mac App Store).
|
||||
- If you want Sparkle updates, you'll typically ship a notarized .zip + an appcast feed.
|
||||
EOF
|
||||
}
|
||||
|
||||
list_identities_and_profiles() {
|
||||
echo "==> [$(ts)] macOS code signing identities (Keychain)"
|
||||
security find-identity -p codesigning -v || true
|
||||
|
||||
echo
|
||||
echo "==> [$(ts)] notarytool credential profiles"
|
||||
echo "Note: this Xcode notarytool version does not provide a 'list-profiles' command."
|
||||
echo "If you forgot the profile name you created, check Keychain Access or re-run:"
|
||||
echo " xcrun notarytool store-credentials \"<profile-name>\" --apple-id \"you@example.com\" --team-id \"TEAMID\""
|
||||
|
||||
echo
|
||||
echo "==> [$(ts)] Provisioning profiles (macOS)"
|
||||
prof_dir="${HOME}/Library/MobileDevice/Provisioning Profiles"
|
||||
if [[ -d "${prof_dir}" ]]; then
|
||||
ls -la "${prof_dir}" | head -n 50
|
||||
else
|
||||
echo "(none found at '${prof_dir}')"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
echo "Error: This script is for macOS only." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then
|
||||
echo "Error: Xcode Command Line Tools not found." >&2
|
||||
echo "Install them first: xcode-select --install" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
do_run=0
|
||||
config="Release"
|
||||
do_clean=0
|
||||
do_deploy=1
|
||||
do_dmg=0
|
||||
build_dir=""
|
||||
identity=""
|
||||
entitlements=""
|
||||
notary_profile=""
|
||||
skip_notarize=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--run) do_run=1; shift ;;
|
||||
--release) config="Release"; shift ;;
|
||||
--debug) config="Debug"; shift ;;
|
||||
--clean) do_clean=1; shift ;;
|
||||
--deploy) do_deploy=1; shift ;;
|
||||
--no-deploy) do_deploy=0; shift ;;
|
||||
--dmg) do_dmg=1; shift ;;
|
||||
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
||||
--identity) identity="${2:-}"; shift 2 ;;
|
||||
--entitlements) entitlements="${2:-}"; shift 2 ;;
|
||||
--notary-profile) notary_profile="${2:-}"; shift 2 ;;
|
||||
--skip-notarize) skip_notarize=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$do_run" -eq 0 ]]; then
|
||||
usage
|
||||
echo
|
||||
list_identities_and_profiles
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$build_dir" ]]; then
|
||||
lc_config="$(echo "$config" | tr '[:upper:]' '[:lower:]')"
|
||||
build_dir="${repo_root}/cmake-build-macos-${lc_config}"
|
||||
fi
|
||||
|
||||
app_path="${build_dir}/strawberry.app"
|
||||
bin_path="${app_path}/Contents/MacOS/strawberry"
|
||||
zip_path="${build_dir}/strawberry-notarize.zip"
|
||||
dmg_path=""
|
||||
|
||||
notarize_and_maybe_staple() {
|
||||
local file_path="$1"
|
||||
local label="$2"
|
||||
local do_staple="${3:-1}"
|
||||
|
||||
echo "==> [$(ts)] Notarizing ${label}"
|
||||
local out
|
||||
out="$(mktemp -t notarytool-submit.XXXXXX)"
|
||||
xcrun notarytool submit "$file_path" --keychain-profile "$notary_profile" --wait --output-format plist --no-progress >"$out"
|
||||
|
||||
local submit_id submit_status
|
||||
submit_id="$(/usr/bin/plutil -extract id raw -o - "$out" 2>/dev/null || true)"
|
||||
submit_status="$(/usr/bin/plutil -extract status raw -o - "$out" 2>/dev/null || true)"
|
||||
rm -f "$out" || true
|
||||
|
||||
if [[ -z "$submit_id" ]]; then
|
||||
echo "Error: could not parse notarization submission id for ${label}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Notary submission id: $submit_id"
|
||||
echo "==> [$(ts)] Notary status: $submit_status"
|
||||
|
||||
if [[ "$submit_status" != "Accepted" ]]; then
|
||||
echo "Error: notarization failed for ${label} with status '$submit_status'. Fetching log..." >&2
|
||||
xcrun notarytool log "$submit_id" --keychain-profile "$notary_profile" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$do_staple" -eq 1 ]]; then
|
||||
echo "==> [$(ts)] Stapling ${label}"
|
||||
xcrun stapler staple "$file_path"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -z "$identity" ]]; then
|
||||
echo "Error: Missing --identity (Developer ID Application identity)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$skip_notarize" -eq 0 && -z "$notary_profile" ]]; then
|
||||
echo "Error: Missing --notary-profile (or pass --skip-notarize)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Building Strawberry"
|
||||
build_args=( "--release" )
|
||||
if [[ "$config" == "Debug" ]]; then build_args=( "--debug" ); fi
|
||||
if [[ "$do_clean" -eq 1 ]]; then build_args+=( "--clean" ); fi
|
||||
if [[ -n "$build_dir" ]]; then build_args+=( "--build-dir" "$build_dir" ); fi
|
||||
if [[ "$do_deploy" -eq 1 ]]; then build_args+=( "--deploy" ); fi
|
||||
|
||||
"${repo_root}/build_tools/macos/build_app.sh" "${build_args[@]}"
|
||||
|
||||
if [[ ! -x "$bin_path" ]]; then
|
||||
echo "Error: built app not found at: $app_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Codesigning (hardened runtime)"
|
||||
codesign_args=( --force --timestamp --options runtime --sign "$identity" )
|
||||
if [[ -n "$entitlements" ]]; then
|
||||
codesign_args+=( --entitlements "$entitlements" )
|
||||
fi
|
||||
|
||||
# Clean up any leftover codesign temp files from previous interrupted runs.
|
||||
# codesign may create *.cstemp alongside binaries while updating signatures.
|
||||
find "$app_path" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do
|
||||
rm -f "$f" || true
|
||||
done
|
||||
|
||||
# Clear macOS provenance/quarantine metadata which can interfere with modifying files in-place.
|
||||
xattr -dr com.apple.provenance "$app_path" >/dev/null 2>&1 || true
|
||||
xattr -dr com.apple.quarantine "$app_path" >/dev/null 2>&1 || true
|
||||
|
||||
# Sign nested code first, then frameworks, then the main app bundle.
|
||||
#
|
||||
# Important: do NOT codesign individual files *inside* a .framework bundle (e.g. Sparkle.framework/Sparkle),
|
||||
# because codesign expects frameworks to be signed as bundles and may error with
|
||||
# "bundle format is ambiguous (could be app or framework)".
|
||||
|
||||
# 1) Sign dylibs and standalone executables that are NOT inside a .framework/.app/.xpc bundle.
|
||||
find "$app_path" -type f \( -name "*.dylib" -o -name "*.so" -o -perm -111 \) \
|
||||
! -name "*.cstemp" \
|
||||
! -path "*/Contents/Frameworks/*.framework/*" \
|
||||
! -path "*/Contents/Frameworks/*.app/*" \
|
||||
! -path "*/Contents/Frameworks/*.xpc/*" \
|
||||
! -path "*/Contents/PlugIns/*.framework/*" \
|
||||
! -path "*/Contents/PlugIns/*.app/*" \
|
||||
! -path "*/Contents/PlugIns/*.xpc/*" \
|
||||
-print0 | while IFS= read -r -d '' f; do
|
||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
||||
done
|
||||
|
||||
# 2) Sign nested helper apps and XPC services (Sparkle ships these inside its framework).
|
||||
find "$app_path" -type d \( -name "*.xpc" -o -name "*.app" \) -print0 2>/dev/null | while IFS= read -r -d '' b; do
|
||||
codesign "${codesign_args[@]}" "$b" >/dev/null
|
||||
done
|
||||
|
||||
# 2b) Sparkle.framework contains a standalone helper executable "Autoupdate" under Versions/* that is
|
||||
# not inside an .app or .xpc bundle. Notarization requires it be signed with Developer ID + timestamp.
|
||||
sparkle_fw="$app_path/Contents/Frameworks/Sparkle.framework"
|
||||
if [[ -d "$sparkle_fw" ]]; then
|
||||
find "$sparkle_fw/Versions" -type f -perm -111 \
|
||||
! -name "*.cstemp" \
|
||||
! -path "*/_CodeSignature/*" \
|
||||
-print0 2>/dev/null | while IFS= read -r -d '' f; do
|
||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
||||
done
|
||||
fi
|
||||
|
||||
# 3) Sign frameworks as bundles.
|
||||
find "$app_path/Contents/Frameworks" "$app_path/Contents/PlugIns" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
|
||||
codesign "${codesign_args[@]}" "$fw" >/dev/null
|
||||
done
|
||||
|
||||
# 4) Finally sign the main app.
|
||||
codesign "${codesign_args[@]}" "$app_path" >/dev/null
|
||||
|
||||
echo "==> [$(ts)] Verifying codesign"
|
||||
codesign --verify --deep --strict --verbose=2 "$app_path"
|
||||
|
||||
echo "==> [$(ts)] Creating zip for notarization"
|
||||
rm -f "$zip_path"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$app_path" "$zip_path"
|
||||
|
||||
if [[ "$skip_notarize" -eq 0 ]]; then
|
||||
# ZIP archives cannot be stapled; notarization is still useful for distribution and Sparkle.
|
||||
notarize_and_maybe_staple "$zip_path" "ZIP" 0
|
||||
echo "==> [$(ts)] Stapling app"
|
||||
xcrun stapler staple "$app_path"
|
||||
fi
|
||||
|
||||
if [[ "$do_dmg" -eq 1 ]]; then
|
||||
echo "==> [$(ts)] Building DMG (from already-signed app; no redeploy)"
|
||||
if ! command -v create-dmg >/dev/null 2>&1; then
|
||||
echo "Error: create-dmg not found. Install it with Homebrew (it's in Brewfile):" >&2
|
||||
echo " ./build_tools/macos/install_brew_deps.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build a versioned DMG name using Info.plist (falls back to Strawberry version constant).
|
||||
plist="${app_path}/Contents/Info.plist"
|
||||
bundle_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$plist" 2>/dev/null || true)"
|
||||
if [[ -z "${bundle_version}" ]]; then
|
||||
bundle_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist" 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -z "${bundle_version}" ]]; then
|
||||
bundle_version="unknown"
|
||||
fi
|
||||
arch="$(uname -m)"
|
||||
dmg_path="${build_dir}/strawberry-${bundle_version}-${arch}.dmg"
|
||||
|
||||
rm -f "$dmg_path"
|
||||
(
|
||||
cd "$build_dir"
|
||||
create-dmg \
|
||||
--volname strawberry \
|
||||
--background "${repo_root}/dist/macos/dmg_background.png" \
|
||||
--app-drop-link 450 218 \
|
||||
--icon strawberry.app 150 218 \
|
||||
--window-size 600 450 \
|
||||
"$(basename "$dmg_path")" \
|
||||
strawberry.app
|
||||
)
|
||||
if [[ -z "$dmg_path" ]]; then
|
||||
echo "Error: DMG was not created in $build_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Codesigning DMG"
|
||||
codesign --force --timestamp --sign "$identity" "$dmg_path"
|
||||
|
||||
if [[ "$skip_notarize" -eq 0 ]]; then
|
||||
notarize_and_maybe_staple "$dmg_path" "DMG" 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Gatekeeper assessment"
|
||||
spctl -a -vv --type execute "$app_path" || true
|
||||
|
||||
echo
|
||||
echo "Done."
|
||||
echo "App: $app_path"
|
||||
echo "Zip: $zip_path"
|
||||
if [[ -n "${dmg_path}" ]]; then
|
||||
echo "DMG: $dmg_path"
|
||||
fi
|
||||
|
||||
119
build_tools/macos/install_brew_deps.sh
Executable file
119
build_tools/macos/install_brew_deps.sh
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
||||
|
||||
ts() { date +"%H:%M:%S"; }
|
||||
|
||||
run_with_heartbeat() {
|
||||
local desc="$1"
|
||||
shift
|
||||
|
||||
local start now elapsed hb_pid
|
||||
start="$(date +%s)"
|
||||
|
||||
echo "==> [$(ts)] ${desc}"
|
||||
|
||||
# Heartbeat: print elapsed time periodically in case the underlying command is quiet
|
||||
(
|
||||
while true; do
|
||||
sleep 20
|
||||
now="$(date +%s)"
|
||||
elapsed="$((now - start))"
|
||||
echo " [$(ts)] ... still working (${elapsed}s elapsed) ..."
|
||||
done
|
||||
) &
|
||||
hb_pid="$!"
|
||||
|
||||
set +e
|
||||
"$@"
|
||||
local rc=$?
|
||||
set -e
|
||||
|
||||
kill "$hb_pid" >/dev/null 2>&1 || true
|
||||
wait "$hb_pid" >/dev/null 2>&1 || true
|
||||
|
||||
now="$(date +%s)"
|
||||
elapsed="$((now - start))"
|
||||
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
echo "Error: '${desc}' failed after ${elapsed}s (exit $rc)." >&2
|
||||
return "$rc"
|
||||
fi
|
||||
|
||||
echo "==> [$(ts)] Done: ${desc} (${elapsed}s)"
|
||||
}
|
||||
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
echo "Error: This script is for macOS only." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
echo "Error: Homebrew ('brew') not found in PATH." >&2
|
||||
echo "Install Homebrew first: https://brew.sh/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Homebrew taps are git clones; local formula changes must be committed to be visible.
|
||||
if command -v git >/dev/null 2>&1 && git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
if git -C "$repo_root" status --porcelain Formula/ | grep -q .; then
|
||||
echo "Error: You have uncommitted changes under Formula/." >&2
|
||||
echo "Homebrew taps are git clones, so uncommitted formulae won't be visible to 'brew tap'." >&2
|
||||
echo "Commit your changes, then re-run this script." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Optional: disable auto-update for faster, more predictable runs.
|
||||
export HOMEBREW_NO_AUTO_UPDATE="${HOMEBREW_NO_AUTO_UPDATE:-1}"
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
echo "==> [$(ts)] Using repo: $repo_root"
|
||||
|
||||
# Strawberry includes local Homebrew formulae under Formula/.
|
||||
# Homebrew requires formulae to be in a tap; we tap this repo via file:// and then
|
||||
# update the tap clone to the latest commit (without untapping, since Homebrew may
|
||||
# refuse to untap when formulae from this tap are installed).
|
||||
run_with_heartbeat "Ensuring local tap exists: strawberry/local" bash -lc \
|
||||
"brew tap | grep -q '^strawberry/local$' || brew tap strawberry/local 'file://$repo_root' >/dev/null"
|
||||
|
||||
run_with_heartbeat "Refreshing strawberry/local tap clone" bash -lc '
|
||||
tap_repo="$(brew --repo strawberry/local)"
|
||||
cd "$tap_repo"
|
||||
# Make sure the remote points at the current local repo path.
|
||||
git remote set-url origin "file://'"$repo_root"'"
|
||||
git fetch -q origin
|
||||
default_ref="$(git symbolic-ref -q --short refs/remotes/origin/HEAD || true)"
|
||||
if [ -z "$default_ref" ]; then
|
||||
default_ref="origin/master"
|
||||
fi
|
||||
git reset --hard -q "$default_ref"
|
||||
'
|
||||
|
||||
for f in kdsingleapplication-qt6 qtsparkle-qt6 sparkle-framework libgpod macdeploycheck; do
|
||||
if ! brew info "strawberry/local/${f}" >/dev/null 2>&1; then
|
||||
echo "Error: Missing formula strawberry/local/${f} in the tapped repo." >&2
|
||||
echo "If you recently added/changed formulae, ensure they are committed, then refresh the tap:" >&2
|
||||
echo " git -C \"$(brew --repo strawberry/local)\" pull --ff-only" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
run_with_heartbeat "Installing dependencies from Brewfile" \
|
||||
brew bundle install --file "$repo_root/Brewfile" --verbose
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done.
|
||||
|
||||
Notes for packaging (optional):
|
||||
- The CMake target 'deploy' expects these env vars for bundling GIO + GStreamer bits:
|
||||
export GIO_EXTRA_MODULES="\$(brew --prefix)/lib/gio/modules"
|
||||
export GST_PLUGIN_SCANNER="\$(brew --prefix gstreamer)/libexec/gstreamer-1.0/gst-plugin-scanner"
|
||||
export GST_PLUGIN_PATH="\$(brew --prefix)/lib/gstreamer-1.0"
|
||||
|
||||
EOF
|
||||
|
||||
@@ -1,43 +1,104 @@
|
||||
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||
# NOTE: Packaging helpers should not be REQUIRED at configure time.
|
||||
# Missing tools should simply disable the related custom targets.
|
||||
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin)
|
||||
if(MACDEPLOYQT_EXECUTABLE)
|
||||
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing macdeployqt executable.")
|
||||
endif()
|
||||
|
||||
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin)
|
||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||
message(STATUS "Found macdeploycheck: ${MACDEPLOYCHECK_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing macdeploycheck executable.")
|
||||
message(STATUS "macdeploycheck not found (optional): 'deploycheck' target will be unavailable.")
|
||||
endif()
|
||||
|
||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg REQUIRED)
|
||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg)
|
||||
if(CREATEDMG_EXECUTABLE)
|
||||
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing create-dmg executable.")
|
||||
endif()
|
||||
|
||||
set(_SPARKLE_FRAMEWORK_DIR "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "")
|
||||
if(SPARKLE)
|
||||
# SPARKLE may be either the framework directory or the framework binary path.
|
||||
get_filename_component(_sparkle_link "${SPARKLE}" ABSOLUTE)
|
||||
get_filename_component(_sparkle_real "${SPARKLE}" REALPATH)
|
||||
if(_sparkle_link MATCHES "Sparkle\\.framework$")
|
||||
set(_SPARKLE_FRAMEWORK_DIR "${_sparkle_real}")
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "${_sparkle_link}/Versions/B/Sparkle")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "${_sparkle_real}/Versions/B/Sparkle")
|
||||
else()
|
||||
# Assume it's the framework binary path:
|
||||
# .../Sparkle.framework/Versions/B/Sparkle
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "${_sparkle_link}")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "${_sparkle_real}")
|
||||
get_filename_component(_sparkle_b_dir "${_SPARKLE_ORIGINAL_BIN_REAL}" DIRECTORY) # .../Versions/B
|
||||
get_filename_component(_sparkle_versions_dir "${_sparkle_b_dir}" DIRECTORY) # .../Versions
|
||||
get_filename_component(_SPARKLE_FRAMEWORK_DIR "${_sparkle_versions_dir}" DIRECTORY) # .../Sparkle.framework
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${_SPARKLE_FRAMEWORK_DIR}" OR NOT EXISTS "${_SPARKLE_ORIGINAL_BIN_REAL}")
|
||||
set(_SPARKLE_FRAMEWORK_DIR "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_LINK "")
|
||||
set(_SPARKLE_ORIGINAL_BIN_REAL "")
|
||||
else()
|
||||
message(STATUS "Sparkle.framework found: ${_SPARKLE_FRAMEWORK_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MACDEPLOYQT_EXECUTABLE)
|
||||
|
||||
if(APPLE_DEVELOPER_ID)
|
||||
set(MACDEPLOYQT_CODESIGN -codesign=${APPLE_DEVELOPER_ID})
|
||||
set(CREATEDMG_CODESIGN --codesign ${APPLE_DEVELOPER_ID})
|
||||
endif()
|
||||
# Note: We intentionally do NOT codesign during the CMake 'deploy'/'dmg' targets.
|
||||
# macdeployqt can optionally sign, but passing identities safely through Ninja's /bin/sh wrapper is brittle.
|
||||
# This repo's signing/notarization pipeline is handled in build_tools/macos/build_sign_notarize.sh instead.
|
||||
if(CREATEDMG_SKIP_JENKINS)
|
||||
set(CREATEDMG_SKIP_JENKINS_ARG "--skip-jenkins")
|
||||
endif()
|
||||
|
||||
add_custom_target(deploy
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/{Frameworks,Resources}
|
||||
set(_deploy_commands
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Frameworks
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources
|
||||
COMMAND cp -v ${CMAKE_BINARY_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
|
||||
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS strawberry
|
||||
)
|
||||
|
||||
if(_SPARKLE_FRAMEWORK_DIR)
|
||||
list(APPEND _deploy_commands
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/bundle_sparkle.sh ${CMAKE_BINARY_DIR}/strawberry.app ${_SPARKLE_FRAMEWORK_DIR} ${_SPARKLE_ORIGINAL_BIN_LINK} ${_SPARKLE_ORIGINAL_BIN_REAL}
|
||||
)
|
||||
endif()
|
||||
|
||||
list(APPEND _deploy_commands
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner
|
||||
)
|
||||
|
||||
# Make 'deploy' incremental:
|
||||
# - add_custom_target() is always out-of-date, so it reruns every time.
|
||||
# - using a stamp file makes Ninja/Make skip deploy when inputs haven't changed.
|
||||
set(_deploy_stamp "${CMAKE_BINARY_DIR}/deploy_app_bundle.stamp")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_deploy_stamp}"
|
||||
${_deploy_commands}
|
||||
COMMAND ${CMAKE_COMMAND} -E touch "${_deploy_stamp}"
|
||||
COMMENT "Deploying app bundle (bundling Sparkle/GStreamer + macdeployqt)"
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS
|
||||
strawberry
|
||||
"${CMAKE_BINARY_DIR}/dist/macos/Info.plist"
|
||||
"${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns"
|
||||
"${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh"
|
||||
"${CMAKE_SOURCE_DIR}/dist/macos/bundle_sparkle.sh"
|
||||
)
|
||||
|
||||
add_custom_target(deploy DEPENDS "${_deploy_stamp}")
|
||||
|
||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||
add_custom_target(deploycheck
|
||||
COMMAND ${MACDEPLOYCHECK_EXECUTABLE} strawberry.app
|
||||
@@ -45,8 +106,9 @@ if(MACDEPLOYQT_EXECUTABLE)
|
||||
endif()
|
||||
if(CREATEDMG_EXECUTABLE)
|
||||
add_custom_target(dmg
|
||||
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_CODESIGN} ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
||||
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS deploy
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
31
cmake/FindRapidJSON.cmake
Normal file
31
cmake/FindRapidJSON.cmake
Normal file
@@ -0,0 +1,31 @@
|
||||
# Try to find RapidJSON (header-only).
|
||||
#
|
||||
# This project uses `find_package(RapidJSON)` and expects:
|
||||
# - RapidJSON_FOUND
|
||||
# - RapidJSON_INCLUDE_DIRS
|
||||
#
|
||||
# Homebrew's `rapidjson` formula commonly installs headers to:
|
||||
# /opt/homebrew/include/rapidjson
|
||||
# but does not always ship a `RapidJSONConfig.cmake`, so we provide this
|
||||
# Find-module fallback.
|
||||
|
||||
find_path(RapidJSON_INCLUDE_DIR
|
||||
NAMES rapidjson/document.h
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(RapidJSON
|
||||
REQUIRED_VARS RapidJSON_INCLUDE_DIR
|
||||
)
|
||||
|
||||
if(RapidJSON_FOUND)
|
||||
set(RapidJSON_INCLUDE_DIRS "${RapidJSON_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
if(RapidJSON_FOUND AND NOT TARGET RapidJSON::RapidJSON)
|
||||
add_library(RapidJSON::RapidJSON INTERFACE IMPORTED)
|
||||
set_target_properties(RapidJSON::RapidJSON PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${RapidJSON_INCLUDE_DIR}"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
set(summary_willbuild "")
|
||||
set(summary_willnotbuild "")
|
||||
|
||||
# On some platforms (notably macOS via Homebrew), many "optional" dependencies are
|
||||
# not installed by default. Historically, Strawberry treated missing optional deps
|
||||
# as a hard error when the option defaulted to ON, which makes first-time builds
|
||||
# frustrating.
|
||||
#
|
||||
# This toggle controls that behavior:
|
||||
# - ON => missing optional deps abort the configure (packager/CI-friendly)
|
||||
# - OFF => missing optional deps auto-disable the component (dev-friendly)
|
||||
set(_optional_components_fatal_default ON)
|
||||
if(APPLE)
|
||||
set(_optional_components_fatal_default OFF)
|
||||
endif()
|
||||
option(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL
|
||||
"If ON, missing optional component dependencies are fatal (otherwise components auto-disable)"
|
||||
${_optional_components_fatal_default}
|
||||
)
|
||||
|
||||
macro(optional_component_summary_add name test)
|
||||
if (${test})
|
||||
list(APPEND summary_willbuild ${name})
|
||||
@@ -80,8 +97,13 @@ function(optional_component name default description)
|
||||
set(text "${description} (missing ${deplist_text})")
|
||||
|
||||
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
|
||||
|
||||
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
||||
if(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL)
|
||||
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
||||
else()
|
||||
message(STATUS "${text} - disabling ${option_variable}")
|
||||
set(${option_variable} OFF CACHE BOOL "${description}" FORCE)
|
||||
return()
|
||||
endif()
|
||||
|
||||
else()
|
||||
set(${have_variable} ON PARENT_SCOPE)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 2)
|
||||
set(STRAWBERRY_VERSION_PATCH 14)
|
||||
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" "$@"
|
||||
|
||||
2
debian/control
vendored
2
debian/control
vendored
@@ -60,7 +60,7 @@ 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
|
||||
|
||||
15
dist/CMakeLists.txt
vendored
15
dist/CMakeLists.txt
vendored
@@ -9,6 +9,21 @@ if(APPLE)
|
||||
else()
|
||||
set(LSMinimumSystemVersion 12.0)
|
||||
endif()
|
||||
|
||||
# Sparkle (macOS updates)
|
||||
# These values are embedded into Info.plist and control where the app checks for updates.
|
||||
# Downstream builders can override on the CMake command line:
|
||||
# -DSPARKLE_FEED_URL="https://example.com/appcast.xml"
|
||||
# -DSPARKLE_PUBLIC_ED25519_KEY="base64=="
|
||||
#
|
||||
# Defaults preserve upstream behavior, but are intentionally configurable for third-party builds.
|
||||
if(NOT DEFINED SPARKLE_FEED_URL)
|
||||
set(SPARKLE_FEED_URL "https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@")
|
||||
endif()
|
||||
if(NOT DEFINED SPARKLE_PUBLIC_ED25519_KEY)
|
||||
set(SPARKLE_PUBLIC_ED25519_KEY "/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=")
|
||||
endif()
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
||||
endif()
|
||||
|
||||
|
||||
4
dist/macos/Info.plist.in
vendored
4
dist/macos/Info.plist.in
vendored
@@ -35,9 +35,9 @@
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>@LSMinimumSystemVersion@</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@</string>
|
||||
<string>@SPARKLE_FEED_URL@</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=</string>
|
||||
<string>@SPARKLE_PUBLIC_ED25519_KEY@</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
85
dist/macos/bundle_sparkle.sh
vendored
Executable file
85
dist/macos/bundle_sparkle.sh
vendored
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
bundledir="${1:-}"
|
||||
sparkle_framework_dir="${2:-}"
|
||||
sparkle_bin_link="${3:-}"
|
||||
sparkle_bin_real="${4:-}"
|
||||
|
||||
if [[ -z "$bundledir" || -z "$sparkle_framework_dir" ]]; then
|
||||
echo "Usage: $0 <bundledir> <sparkle_framework_dir> [sparkle_bin_link] [sparkle_bin_real]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -d "$sparkle_framework_dir" ]]; then
|
||||
echo "Sparkle.framework dir not found: $sparkle_framework_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
src_framework_dir="$sparkle_framework_dir"
|
||||
|
||||
# Homebrew often provides /opt/homebrew/Frameworks/Sparkle.framework where Versions/* are symlinks
|
||||
# pointing back into the Cellar. Copying that verbatim breaks inside an app bundle.
|
||||
# Resolve to the real Cellar framework root via Versions/Current.
|
||||
if [[ -e "${sparkle_framework_dir}/Versions/Current" ]]; then
|
||||
current_real="$(cd "${sparkle_framework_dir}/Versions/Current" && pwd -P)"
|
||||
# current_real is .../Sparkle.framework/Versions/B (or similar)
|
||||
src_framework_dir="$(cd "${current_real}/../.." && pwd -P)"
|
||||
fi
|
||||
|
||||
dst_framework="${bundledir}/Contents/Frameworks/Sparkle.framework"
|
||||
main_bin="${bundledir}/Contents/MacOS/strawberry"
|
||||
qtsparkle_dylib="${bundledir}/Contents/Frameworks/libqtsparkle-qt6.dylib"
|
||||
|
||||
mkdir -p "${bundledir}/Contents/Frameworks"
|
||||
|
||||
echo "Bundling Sparkle.framework -> ${dst_framework}"
|
||||
rm -rf "${dst_framework}"
|
||||
# Use ditto to preserve the framework's internal symlinks/structure.
|
||||
ditto "${src_framework_dir}" "${dst_framework}"
|
||||
|
||||
# Prefer the canonical framework binary path.
|
||||
dst_bin="${dst_framework}/Versions/Current/Sparkle"
|
||||
if [[ ! -e "${dst_bin}" ]]; then
|
||||
echo "Error: Sparkle binary missing at ${dst_bin}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sparkle_rpath="@rpath/Sparkle.framework/Versions/Current/Sparkle"
|
||||
|
||||
# Sanity check: top-level Sparkle entry should be a symlink (not a copied Mach-O file).
|
||||
if [[ -e "${dst_framework}/Sparkle" && ! -L "${dst_framework}/Sparkle" ]]; then
|
||||
echo "Warning: ${dst_framework}/Sparkle is not a symlink (unexpected). This can confuse codesign." >&2
|
||||
fi
|
||||
|
||||
echo "Fixing Sparkle.framework install name"
|
||||
install_name_tool -id "${sparkle_rpath}" "${dst_bin}"
|
||||
|
||||
echo "Ensuring main binary has Frameworks rpath"
|
||||
install_name_tool -add_rpath "@executable_path/../Frameworks" "${main_bin}" || true
|
||||
|
||||
echo "Rewriting Sparkle.framework references to @rpath"
|
||||
# Try to rewrite a few common Homebrew Sparkle install names as well, because the
|
||||
# recorded install name may differ from the path returned by CMake's find_library.
|
||||
old_candidates=(
|
||||
"${sparkle_bin_link}"
|
||||
"${sparkle_bin_real}"
|
||||
"/opt/homebrew/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
||||
"/opt/homebrew/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
||||
"/opt/homebrew/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
||||
"/opt/homebrew/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
||||
"/usr/local/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
||||
"/usr/local/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
||||
"/usr/local/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
||||
"/usr/local/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
||||
)
|
||||
|
||||
for old in "${old_candidates[@]}"; do
|
||||
if [[ -n "${old}" ]]; then
|
||||
install_name_tool -change "${old}" "${sparkle_rpath}" "${main_bin}" || true
|
||||
if [[ -f "${qtsparkle_dylib}" ]]; then
|
||||
install_name_tool -change "${old}" "${sparkle_rpath}" "${qtsparkle_dylib}" || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<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 and ListenBrainz</li>
|
||||
@@ -51,6 +51,9 @@
|
||||
</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"/>
|
||||
|
||||
4
dist/unix/strawberry.1
vendored
4
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
|
||||
|
||||
3
dist/unix/strawberry.spec.in
vendored
3
dist/unix/strawberry.spec.in
vendored
@@ -93,8 +93,7 @@ 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 and ListenBrainz
|
||||
|
||||
38
dist/windows/strawberry.nsi.in
vendored
38
dist/windows/strawberry.nsi.in
vendored
@@ -477,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"
|
||||
@@ -494,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"
|
||||
@@ -505,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"
|
||||
@@ -512,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
|
||||
@@ -1019,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"
|
||||
@@ -1036,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"
|
||||
@@ -1047,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"
|
||||
@@ -1054,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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -686,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;
|
||||
}
|
||||
|
||||
@@ -705,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);
|
||||
@@ -719,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(' '));
|
||||
}
|
||||
@@ -1074,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:
|
||||
@@ -1162,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;
|
||||
|
||||
}
|
||||
@@ -1218,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;
|
||||
@@ -1246,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());
|
||||
@@ -1341,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) {
|
||||
|
||||
@@ -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
|
||||
@@ -33,7 +33,7 @@ constexpr char kFileFilter[] =
|
||||
"*.mod *.s3m *.xm *.it "
|
||||
"*.spc *.vgm";
|
||||
|
||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
|
||||
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
|
||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm *.webp)");
|
||||
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm *.webp)");
|
||||
|
||||
#endif // FILEFILTERCONSTANTS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILENAMECONSTANTS_H
|
||||
#define FILENAMECONSTANTS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILESYSTEMCONSTANTS_H
|
||||
#define FILESYSTEMCONSTANTS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GLOBALSHORTCUTSSETTINGS_H
|
||||
#define GLOBALSHORTCUTSSETTINGS_H
|
||||
@@ -26,6 +26,6 @@ constexpr char kSettingsGroup[] = "GlobalShortcuts";
|
||||
constexpr char kUseKGlobalAccel[] = "use_kglobalaccel";
|
||||
constexpr char kUseX11[] = "use_x11";
|
||||
|
||||
} // namespace
|
||||
} // namespace GlobalShortcutsSettings
|
||||
|
||||
#endif // GLOBALSHORTCUTSSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef LYRICSSETTINGS_H
|
||||
#define LYRICSSETTINGS_H
|
||||
@@ -25,6 +25,6 @@ namespace LyricsSettings {
|
||||
constexpr char kSettingsGroup[] = "Lyrics";
|
||||
constexpr char kProviders[] = "providers";
|
||||
|
||||
} // namespace
|
||||
} // namespace LyricsSettings
|
||||
|
||||
#endif // LYRICSSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MAINWINDOWSETTINGS_H
|
||||
#define MAINWINDOWSETTINGS_H
|
||||
@@ -32,6 +32,6 @@ constexpr char kGeometry[] = "geometry";
|
||||
constexpr char kSplitterState[] = "splitter_state";
|
||||
constexpr char kDoNotShowSponsorMessage[] = "do_not_show_sponsor_message";
|
||||
|
||||
} // namespace
|
||||
} // namespace MainWindowSettings
|
||||
|
||||
#endif // MAINWINDOWSETTINGS_H
|
||||
#endif // MAINWINDOWSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MOODBARSETTINGS_H
|
||||
#define MOODBARSETTINGS_H
|
||||
@@ -38,6 +38,6 @@ constexpr char kShow[] = "show";
|
||||
constexpr char kStyle[] = "style";
|
||||
constexpr char kSave[] = "save";
|
||||
|
||||
} // namespace
|
||||
} // namespace MoodbarSettings
|
||||
|
||||
#endif // MOODBARSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef NETWORKPROXYSETTINGS_H
|
||||
#define NETWORKPROXYSETTINGS_H
|
||||
@@ -32,6 +32,6 @@ constexpr char kUsername[] = "username";
|
||||
constexpr char kPassword[] = "password";
|
||||
constexpr char kEngine[] = "engine";
|
||||
|
||||
} // namespace
|
||||
} // namespace NetworkProxySettings
|
||||
|
||||
#endif // NETWORKPROXYSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef NOTIFICATIONSSETTINGS_H
|
||||
#define NOTIFICATIONSSETTINGS_H
|
||||
@@ -45,7 +45,7 @@ constexpr char kCustomTextEnabled[] = "CustomTextEnabled";
|
||||
constexpr char kCustomText1[] = "CustomText1";
|
||||
constexpr char kCustomText2[] = "CustomText2";
|
||||
|
||||
} // namespace
|
||||
} // namespace OSDSettings
|
||||
|
||||
namespace OSDPrettySettings {
|
||||
|
||||
@@ -63,7 +63,7 @@ constexpr char kFading[] = "fading";
|
||||
constexpr QRgb kPresetBlue = qRgb(102, 150, 227);
|
||||
constexpr QRgb kPresetRed = qRgb(202, 22, 16);
|
||||
|
||||
} // namespace
|
||||
} // namespace OSDPrettySettings
|
||||
|
||||
namespace DiscordRPCSettings {
|
||||
|
||||
@@ -79,6 +79,6 @@ enum class StatusDisplayType {
|
||||
Song
|
||||
};
|
||||
|
||||
} // namespace
|
||||
} // namespace DiscordRPCSettings
|
||||
|
||||
#endif // NOTIFICATIONSSETTINGS_H
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLAYLISTSETTINGS_H
|
||||
#define PLAYLISTSETTINGS_H
|
||||
@@ -63,7 +63,7 @@ constexpr char kLastSaveExtension[] = "last_save_extension";
|
||||
constexpr char kLastSaveAllPath[] = "last_save_all_path";
|
||||
constexpr char kLastSaveAllExtension[] = "last_save_all_extension";
|
||||
|
||||
} // namespace
|
||||
} // namespace PlaylistSettings
|
||||
|
||||
Q_DECLARE_METATYPE(PlaylistSettings::PathType)
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef QOBUZSETTINGS_H
|
||||
#define QOBUZSETTINGS_H
|
||||
@@ -43,6 +43,6 @@ constexpr char kCredentialsId[] = "credentials_id";
|
||||
constexpr char kDeviceId[] = "device_id";
|
||||
constexpr char kUserAuthToken[] = "user_auth_token";
|
||||
|
||||
} // namespace
|
||||
} // namespace QobuzSettings
|
||||
|
||||
#endif // QOBUZSETTINGS_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 SCROBBLERSETTINGS_H
|
||||
#define SCROBBLERSETTINGS_H
|
||||
@@ -35,6 +35,6 @@ constexpr char kStripRemastered[] = "strip_remastered";
|
||||
constexpr char kSources[] = "sources";
|
||||
constexpr char kUserToken[] = "user_token";
|
||||
|
||||
} // namespace
|
||||
} // namespace ScrobblerSettings
|
||||
|
||||
#endif // SCROBBLERSETTINGS_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 SPOTIFYSETTINGS_H
|
||||
#define SPOTIFYSETTINGS_H
|
||||
@@ -38,6 +38,6 @@ constexpr char kRefreshToken[] = "refresh_token";
|
||||
constexpr char kExpiresIn[] = "expires_in";
|
||||
constexpr char kLoginTime[] = "login_time";
|
||||
|
||||
} // namespace
|
||||
} // namespace SpotifySettings
|
||||
|
||||
#endif // SPOTIFYSETTINGS_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 SUBSONICETTINGS_H
|
||||
@@ -41,6 +41,6 @@ constexpr char kUseAlbumIdForAlbumCovers[] = "usealbumidforalbumcovers";
|
||||
constexpr char kServerSideScrobbling[] = "serversidescrobbling";
|
||||
constexpr char kAuthMethod[] = "authmethod";
|
||||
|
||||
} // namespace
|
||||
} // namespace SubsonicSettings
|
||||
|
||||
#endif // SUBSONICETTINGS_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 TIDALSETTINGS_H
|
||||
#define TIDALSETTINGS_H
|
||||
@@ -48,6 +48,6 @@ enum class StreamUrlMethod {
|
||||
PlaybackInfoPostPaywall
|
||||
};
|
||||
|
||||
}
|
||||
} // namespace TidalSettings
|
||||
|
||||
#endif // TIDALSETTINGS_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 TIMECONSTANTS_H
|
||||
#define TIMECONSTANTS_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 TRANSCODERSETTINGS_H
|
||||
@@ -26,4 +26,3 @@ constexpr char kSettingsGroup[] = "Transcoder";
|
||||
}
|
||||
|
||||
#endif // TRANSCODERSETTINGS_H
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ class ContextAlbum : public QWidget {
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
private:
|
||||
|
||||
struct PreviousCover {
|
||||
explicit PreviousCover() : opacity(0.0) {}
|
||||
QImage image;
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
#include "covermanager/albumcoverchoicecontroller.h"
|
||||
#include "lyrics/lyricsfetcher.h"
|
||||
#include "constants/contextsettings.h"
|
||||
#include "constants/timeconstants.h"
|
||||
|
||||
#include "contextview.h"
|
||||
#include "contextalbum.h"
|
||||
@@ -353,7 +354,7 @@ void ContextView::SearchLyrics() {
|
||||
if (lyrics_.isEmpty() && action_show_lyrics_->isChecked() && action_search_lyrics_->isChecked() && !song_playing_.artist().isEmpty() && !song_playing_.title().isEmpty() && !lyrics_tried_ && lyrics_id_ == -1) {
|
||||
lyrics_fetcher_->Clear();
|
||||
lyrics_tried_ = true;
|
||||
lyrics_id_ = static_cast<qint64>(lyrics_fetcher_->Search(song_playing_.effective_albumartist(), song_playing_.artist(), song_playing_.album(), song_playing_.title()));
|
||||
lyrics_id_ = static_cast<qint64>(lyrics_fetcher_->Search(song_playing_.effective_albumartist(), song_playing_.artist(), song_playing_.album(), song_playing_.title(), song_playing_.length_nanosec() / kNsecPerSec));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -65,9 +65,9 @@ class ContextView : public QWidget {
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent*) override;
|
||||
void dragEnterEvent(QDragEnterEvent*) override;
|
||||
void dropEvent(QDropEvent*) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
void dragEnterEvent(QDragEnterEvent *e) override;
|
||||
void dropEvent(QDropEvent *e) override;
|
||||
|
||||
private:
|
||||
void AddActions();
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
#include "lyrics/elyricsnetlyricsprovider.h"
|
||||
#include "lyrics/letraslyricsprovider.h"
|
||||
#include "lyrics/lyricfindlyricsprovider.h"
|
||||
#include "lyrics/lrcliblyricsprovider.h"
|
||||
|
||||
#include "scrobbler/audioscrobbler.h"
|
||||
#include "scrobbler/lastfmscrobbler.h"
|
||||
@@ -117,8 +118,8 @@ using namespace std::chrono_literals;
|
||||
|
||||
class ApplicationImpl {
|
||||
public:
|
||||
explicit ApplicationImpl(Application *app) :
|
||||
tagreader_client_([app](){
|
||||
explicit ApplicationImpl(Application *app)
|
||||
: tagreader_client_([app]() {
|
||||
TagReaderClient *client = new TagReaderClient();
|
||||
app->MoveToNewThread(client);
|
||||
return client;
|
||||
@@ -182,6 +183,7 @@ class ApplicationImpl {
|
||||
lyrics_providers->AddProvider(new ElyricsNetLyricsProvider(lyrics_providers->network()));
|
||||
lyrics_providers->AddProvider(new LetrasLyricsProvider(lyrics_providers->network()));
|
||||
lyrics_providers->AddProvider(new LyricFindLyricsProvider(lyrics_providers->network()));
|
||||
lyrics_providers->AddProvider(new LrcLibLyricsProvider(lyrics_providers->network()));
|
||||
lyrics_providers->ReloadSettings();
|
||||
return lyrics_providers;
|
||||
}),
|
||||
@@ -264,7 +266,7 @@ Application::Application(QObject *parent)
|
||||
|
||||
Application::~Application() {
|
||||
|
||||
qLog(Debug) << "Terminating application";
|
||||
qLog(Debug) << "Terminating application";
|
||||
|
||||
for (QThread *thread : std::as_const(threads_)) {
|
||||
thread->quit();
|
||||
|
||||
@@ -204,7 +204,7 @@ bool CommandlineOptions::Parse() {
|
||||
{ "version", no_argument, nullptr, LongOptions::Version },
|
||||
{ nullptr, 0, nullptr, 0 }
|
||||
#endif
|
||||
};
|
||||
};
|
||||
|
||||
// Parse the arguments
|
||||
bool ok = false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user