Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -130,7 +130,10 @@ InsertBraces: false
|
|||||||
InsertTrailingCommas: None
|
InsertTrailingCommas: None
|
||||||
JavaScriptQuotes: Leave
|
JavaScriptQuotes: Leave
|
||||||
JavaScriptWrapImports: true
|
JavaScriptWrapImports: true
|
||||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
KeepEmptyLines:
|
||||||
|
AtEndOfFile: true
|
||||||
|
AtStartOfBlock: true
|
||||||
|
AtStartOfFile: true
|
||||||
LambdaBodyIndentation: Signature
|
LambdaBodyIndentation: Signature
|
||||||
MacroBlockBegin: ''
|
MacroBlockBegin: ''
|
||||||
MacroBlockEnd: ''
|
MacroBlockEnd: ''
|
||||||
|
|||||||
46
.github/workflows/build.yml
vendored
46
.github/workflows/build.yml
vendored
@@ -134,14 +134,14 @@ jobs:
|
|||||||
run: echo "subdir=$(echo ${{matrix.opensuse_version}} | sed 's/leap:/lp/g' | sed 's/\.//g')" > $GITHUB_OUTPUT
|
run: echo "subdir=$(echo ${{matrix.opensuse_version}} | sed 's/leap:/lp/g' | sed 's/\.//g')" > $GITHUB_OUTPUT
|
||||||
- name: Upload source
|
- name: Upload source
|
||||||
if: matrix.opensuse_version == 'tumbleweed'
|
if: matrix.opensuse_version == 'tumbleweed'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: source
|
name: source
|
||||||
path: |
|
path: |
|
||||||
/usr/src/packages/SOURCES/*.xz
|
/usr/src/packages/SOURCES/*.xz
|
||||||
- name: Upload rpm
|
- name: Upload rpm
|
||||||
if: matrix.opensuse_version != 'tumbleweed'
|
if: matrix.opensuse_version != 'tumbleweed'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: opensuse-${{steps.set-subdir.outputs.subdir}}
|
name: opensuse-${{steps.set-subdir.outputs.subdir}}
|
||||||
path: |
|
path: |
|
||||||
@@ -156,7 +156,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
fedora_version: [ '41', '42', '43' ]
|
fedora_version: [ '42', '43', '44' ]
|
||||||
container:
|
container:
|
||||||
image: fedora:${{matrix.fedora_version}}
|
image: fedora:${{matrix.fedora_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -234,7 +234,7 @@ jobs:
|
|||||||
working-directory: build
|
working-directory: build
|
||||||
run: rpmbuild -ba strawberry.spec
|
run: rpmbuild -ba strawberry.spec
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: fedora-${{matrix.fedora_version}}
|
name: fedora-${{matrix.fedora_version}}
|
||||||
path: |
|
path: |
|
||||||
@@ -333,7 +333,7 @@ jobs:
|
|||||||
run: rpmbuild -ba strawberry.spec
|
run: rpmbuild -ba strawberry.spec
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
if: matrix.openmandriva_version != 'cooker'
|
if: matrix.openmandriva_version != 'cooker'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: openmandriva-${{matrix.openmandriva_version}}
|
name: openmandriva-${{matrix.openmandriva_version}}
|
||||||
path: |
|
path: |
|
||||||
@@ -434,7 +434,7 @@ jobs:
|
|||||||
working-directory: build
|
working-directory: build
|
||||||
run: rpmbuild -ba strawberry.spec
|
run: rpmbuild -ba strawberry.spec
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: mageia-${{matrix.mageia_version}}
|
name: mageia-${{matrix.mageia_version}}
|
||||||
path: |
|
path: |
|
||||||
@@ -528,7 +528,7 @@ jobs:
|
|||||||
- name: Copy deb
|
- name: Copy deb
|
||||||
run: cp ../*.deb .
|
run: cp ../*.deb .
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: debian-${{matrix.debian_version}}
|
name: debian-${{matrix.debian_version}}
|
||||||
path: |
|
path: |
|
||||||
@@ -542,7 +542,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
|
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
||||||
container:
|
container:
|
||||||
image: ubuntu:${{matrix.ubuntu_version}}
|
image: ubuntu:${{matrix.ubuntu_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -596,10 +596,10 @@ jobs:
|
|||||||
qt6-l10n-tools
|
qt6-l10n-tools
|
||||||
rapidjson-dev
|
rapidjson-dev
|
||||||
- name: Install KDSingleApplication
|
- name: Install KDSingleApplication
|
||||||
if: matrix.ubuntu_version != 'noble' && matrix.ubuntu_version != 'plucky'
|
if: matrix.ubuntu_version != 'noble'
|
||||||
run: apt install -y libkdsingleapplication-qt6-dev
|
run: apt install -y libkdsingleapplication-qt6-dev
|
||||||
- name: Build and install KDSingleApplication
|
- name: Build and install KDSingleApplication
|
||||||
if: matrix.ubuntu_version == 'noble' || matrix.ubuntu_version == 'plucky'
|
if: matrix.ubuntu_version == 'noble'
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
|
||||||
cd KDSingleApplication
|
cd KDSingleApplication
|
||||||
@@ -624,7 +624,7 @@ jobs:
|
|||||||
- name: Copy deb
|
- name: Copy deb
|
||||||
run: cp ../*.deb ../*.ddeb .
|
run: cp ../*.deb ../*.ddeb .
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: ubuntu-${{matrix.ubuntu_version}}
|
name: ubuntu-${{matrix.ubuntu_version}}
|
||||||
path: |
|
path: |
|
||||||
@@ -639,7 +639,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
|
ubuntu_version: [ 'noble', 'questing', 'resolute' ]
|
||||||
container:
|
container:
|
||||||
image: ubuntu:${{matrix.ubuntu_version}}
|
image: ubuntu:${{matrix.ubuntu_version}}
|
||||||
steps:
|
steps:
|
||||||
@@ -739,12 +739,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
- name: Free disk space
|
||||||
|
run: |
|
||||||
|
df -h
|
||||||
|
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
|
||||||
|
sudo apt-get clean
|
||||||
|
df -h
|
||||||
- name: Build FreeBSD
|
- name: Build FreeBSD
|
||||||
id: build-freebsd
|
id: build-freebsd
|
||||||
uses: vmactions/freebsd-vm@v1.2.7
|
uses: vmactions/freebsd-vm@v1.3.7
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
mem: 4096
|
mem: 8192
|
||||||
prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio sparsehash rapidjson
|
prepare: pkg install -y git cmake pkgconf boost-libs alsa-lib glib qt6-base qt6-tools sqlite gstreamer1 gstreamer1-plugins chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf2 libgpod fftw3 icu kdsingleapplication googletest pulseaudio sparsehash rapidjson
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
@@ -766,7 +772,7 @@ jobs:
|
|||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Build OpenBSD
|
- name: Build OpenBSD
|
||||||
id: build-openbsd
|
id: build-openbsd
|
||||||
uses: vmactions/openbsd-vm@v1.2.3
|
uses: vmactions/openbsd-vm@v1.3.1
|
||||||
with:
|
with:
|
||||||
usesh: true
|
usesh: true
|
||||||
mem: 4096
|
mem: 4096
|
||||||
@@ -833,13 +839,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Import certificate file
|
- name: Import certificate file
|
||||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false
|
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false
|
||||||
uses: apple-actions/import-codesign-certs@v5
|
uses: apple-actions/import-codesign-certs@v6
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE }}
|
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE }}
|
||||||
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
|
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
|
||||||
|
|
||||||
- name: Download macOS dependencies
|
- name: Download macOS dependencies
|
||||||
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies$(test "${{env.arch}}" = "x86_64" && echo "-intel" || echo "")/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
|
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
|
||||||
|
|
||||||
- name: Extract macOS dependencies
|
- name: Extract macOS dependencies
|
||||||
run: sudo tar -C / -xf strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
|
run: sudo tar -C / -xf strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
|
||||||
@@ -892,7 +898,7 @@ jobs:
|
|||||||
- name: Manually Codesign
|
- name: Manually Codesign
|
||||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
|
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
|
||||||
working-directory: build
|
working-directory: build
|
||||||
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib,libbrotlidec.1.dylib,libsoup-3.0.0.dylib,libnghttp2.14.dylib,libpsl.5.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
||||||
|
|
||||||
- name: Manually Codesign
|
- name: Manually Codesign
|
||||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
|
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
|
||||||
@@ -1642,7 +1648,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: SSH Setup
|
- name: SSH Setup
|
||||||
@@ -1694,7 +1700,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||||
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
|
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: Add artifacts to release
|
- name: Add artifacts to release
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@
|
|||||||
/CMakeSettings.json
|
/CMakeSettings.json
|
||||||
/dist/scripts/maketarball.sh
|
/dist/scripts/maketarball.sh
|
||||||
/debian/changelog
|
/debian/changelog
|
||||||
|
_codeql_detected_source_root
|
||||||
|
|||||||
@@ -163,4 +163,3 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
|
|||||||
Discord_RegisterW(appId, wcommand);
|
Discord_RegisterW(appId, wcommand);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ if(MSVC)
|
|||||||
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
|
list(APPEND COMPILE_OPTIONS -MP -W4 -wd4702)
|
||||||
else()
|
else()
|
||||||
list(APPEND COMPILE_OPTIONS
|
list(APPEND COMPILE_OPTIONS
|
||||||
$<$<COMPILE_LANGUAGE:C>:-std=c11>
|
|
||||||
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
|
|
||||||
-Wall
|
-Wall
|
||||||
-Wextra
|
-Wextra
|
||||||
-Wpedantic
|
-Wpedantic
|
||||||
@@ -255,7 +253,6 @@ find_package(KDSingleApplication-qt${QT_VERSION_MAJOR} 1.1.0 REQUIRED)
|
|||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
find_library(SPARKLE Sparkle)
|
find_library(SPARKLE Sparkle)
|
||||||
#find_package(SPMediaKeyTap REQUIRED)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
@@ -701,6 +698,7 @@ set(SOURCES
|
|||||||
src/lyrics/elyricsnetlyricsprovider.cpp
|
src/lyrics/elyricsnetlyricsprovider.cpp
|
||||||
src/lyrics/letraslyricsprovider.cpp
|
src/lyrics/letraslyricsprovider.cpp
|
||||||
src/lyrics/lyricfindlyricsprovider.cpp
|
src/lyrics/lyricfindlyricsprovider.cpp
|
||||||
|
src/lyrics/lrcliblyricsprovider.cpp
|
||||||
|
|
||||||
src/settings/settingsdialog.cpp
|
src/settings/settingsdialog.cpp
|
||||||
src/settings/settingspage.cpp
|
src/settings/settingspage.cpp
|
||||||
@@ -822,6 +820,8 @@ set(SOURCES
|
|||||||
|
|
||||||
src/fileview/fileview.cpp
|
src/fileview/fileview.cpp
|
||||||
src/fileview/fileviewlist.cpp
|
src/fileview/fileviewlist.cpp
|
||||||
|
src/fileview/fileviewtree.cpp
|
||||||
|
src/fileview/fileviewtreemodel.cpp
|
||||||
|
|
||||||
src/device/devicemanager.cpp
|
src/device/devicemanager.cpp
|
||||||
src/device/devicelister.cpp
|
src/device/devicelister.cpp
|
||||||
@@ -997,6 +997,7 @@ set(HEADERS
|
|||||||
src/lyrics/elyricsnetlyricsprovider.h
|
src/lyrics/elyricsnetlyricsprovider.h
|
||||||
src/lyrics/letraslyricsprovider.h
|
src/lyrics/letraslyricsprovider.h
|
||||||
src/lyrics/lyricfindlyricsprovider.h
|
src/lyrics/lyricfindlyricsprovider.h
|
||||||
|
src/lyrics/lrcliblyricsprovider.h
|
||||||
|
|
||||||
src/settings/settingsdialog.h
|
src/settings/settingsdialog.h
|
||||||
src/settings/settingspage.h
|
src/settings/settingspage.h
|
||||||
@@ -1110,6 +1111,8 @@ set(HEADERS
|
|||||||
|
|
||||||
src/fileview/fileview.h
|
src/fileview/fileview.h
|
||||||
src/fileview/fileviewlist.h
|
src/fileview/fileviewlist.h
|
||||||
|
src/fileview/fileviewtree.h
|
||||||
|
src/fileview/fileviewtreemodel.h
|
||||||
|
|
||||||
src/device/devicemanager.h
|
src/device/devicemanager.h
|
||||||
src/device/devicelister.h
|
src/device/devicelister.h
|
||||||
@@ -1212,6 +1215,10 @@ set(UI
|
|||||||
src/device/deviceviewcontainer.ui
|
src/device/deviceviewcontainer.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(UNIX)
|
||||||
|
optional_source(UNIX SOURCES src/core/unixsignalwatcher.cpp HEADERS src/core/unixsignalwatcher.h)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
optional_source(APPLE
|
optional_source(APPLE
|
||||||
SOURCES
|
SOURCES
|
||||||
@@ -1440,6 +1447,7 @@ optional_source(HAVE_SPOTIFY
|
|||||||
src/spotify/spotifybaserequest.cpp
|
src/spotify/spotifybaserequest.cpp
|
||||||
src/spotify/spotifyrequest.cpp
|
src/spotify/spotifyrequest.cpp
|
||||||
src/spotify/spotifyfavoriterequest.cpp
|
src/spotify/spotifyfavoriterequest.cpp
|
||||||
|
src/spotify/spotifymetadatarequest.cpp
|
||||||
src/settings/spotifysettingspage.cpp
|
src/settings/spotifysettingspage.cpp
|
||||||
src/covermanager/spotifycoverprovider.cpp
|
src/covermanager/spotifycoverprovider.cpp
|
||||||
HEADERS
|
HEADERS
|
||||||
@@ -1447,6 +1455,7 @@ optional_source(HAVE_SPOTIFY
|
|||||||
src/spotify/spotifybaserequest.h
|
src/spotify/spotifybaserequest.h
|
||||||
src/spotify/spotifyrequest.h
|
src/spotify/spotifyrequest.h
|
||||||
src/spotify/spotifyfavoriterequest.h
|
src/spotify/spotifyfavoriterequest.h
|
||||||
|
src/spotify/spotifymetadatarequest.h
|
||||||
src/settings/spotifysettingspage.h
|
src/settings/spotifysettingspage.h
|
||||||
src/covermanager/spotifycoverprovider.h
|
src/covermanager/spotifycoverprovider.h
|
||||||
UI
|
UI
|
||||||
@@ -1461,6 +1470,8 @@ optional_source(HAVE_QOBUZ
|
|||||||
src/qobuz/qobuzrequest.cpp
|
src/qobuz/qobuzrequest.cpp
|
||||||
src/qobuz/qobuzstreamurlrequest.cpp
|
src/qobuz/qobuzstreamurlrequest.cpp
|
||||||
src/qobuz/qobuzfavoriterequest.cpp
|
src/qobuz/qobuzfavoriterequest.cpp
|
||||||
|
src/qobuz/qobuzmetadatarequest.cpp
|
||||||
|
src/qobuz/qobuzcredentialfetcher.cpp
|
||||||
src/settings/qobuzsettingspage.cpp
|
src/settings/qobuzsettingspage.cpp
|
||||||
src/covermanager/qobuzcoverprovider.cpp
|
src/covermanager/qobuzcoverprovider.cpp
|
||||||
HEADERS
|
HEADERS
|
||||||
@@ -1470,6 +1481,8 @@ optional_source(HAVE_QOBUZ
|
|||||||
src/qobuz/qobuzrequest.h
|
src/qobuz/qobuzrequest.h
|
||||||
src/qobuz/qobuzstreamurlrequest.h
|
src/qobuz/qobuzstreamurlrequest.h
|
||||||
src/qobuz/qobuzfavoriterequest.h
|
src/qobuz/qobuzfavoriterequest.h
|
||||||
|
src/qobuz/qobuzmetadatarequest.h
|
||||||
|
src/qobuz/qobuzcredentialfetcher.h
|
||||||
src/settings/qobuzsettingspage.h
|
src/settings/qobuzsettingspage.h
|
||||||
src/covermanager/qobuzcoverprovider.h
|
src/covermanager/qobuzcoverprovider.h
|
||||||
UI
|
UI
|
||||||
|
|||||||
50
Changelog
50
Changelog
@@ -2,6 +2,56 @@ Strawberry Music Player
|
|||||||
=======================
|
=======================
|
||||||
ChangeLog
|
ChangeLog
|
||||||
|
|
||||||
|
Version 1.2.17 (2026.01.18):
|
||||||
|
|
||||||
|
* Avoid re-scan of restored songs unless mtime is changed (#1819)
|
||||||
|
* Skip existing files when organizing if not overwriting (#1484)
|
||||||
|
* Fixed cursor highlight disappearing off-screen when using down cursor (#1489)
|
||||||
|
* Fixed CD playback only working for the first optical drive (#1852)
|
||||||
|
* Fixed possible race-condition when switching tracks (#1863)
|
||||||
|
* Fixed possible file descriptor exhaustion by using shared thread pool (#1687)
|
||||||
|
* Don't automatically sort playlist with the auto sort option before it's fully loaded (#1690)
|
||||||
|
* Fixed network features stop working after computer suspends and resumes (#1521)
|
||||||
|
* Fixed crash on exit after Qobuz login
|
||||||
|
* Added tag editor option to select ID3v2 version (#1861)
|
||||||
|
* Fixed Qobuz authentication and added automatic credential fetching (#1898)
|
||||||
|
* Fixed playback stopping after deleting a song from disk via context menu (#1783)
|
||||||
|
* Added option to restore smart playlists to the defaults (#1848)
|
||||||
|
* Fixed possible race condition in pipeline destructor (#1875)
|
||||||
|
* Fixed buffering issue near track end during gapless playback (#1725)
|
||||||
|
* Fixed duplicate collection entries for the same artist if they have different sort tags (#1899)
|
||||||
|
* Defer playcount and rating tag writes for currently playing Ogg songs to prevent playback shutter (#1816)
|
||||||
|
* Fixed tag editing not working for Opus sort tags (#1929)
|
||||||
|
* Show playlist load errors (#1470)
|
||||||
|
* Fallback to delete if moving to trash fails (#1679)
|
||||||
|
* Prefer filenames with "front" or "cover" in the filename for album cover art for songs outside of the collection (#1745)
|
||||||
|
* Fixed collection enter/return behavior to respect double-click settings (#1691)
|
||||||
|
* Added tree view mode to files tab (#1922)
|
||||||
|
* Include .webp in allowed extensions for album covers (#1941)
|
||||||
|
* Exit gracefully on SIGTERM signal for Unix systems (#1942)
|
||||||
|
* Optimize the collection scanning process by deferring media file validation from the initial directory scan (#1954)
|
||||||
|
* Fixed collection scan not finding new directories in the top level collection directory when the mountpoint is restored (#1914)
|
||||||
|
* Added genre metadata parsing for Tidal, Qobuz and Spotify (#1913)
|
||||||
|
* Allow editing metadata for stream songs (#1913)
|
||||||
|
* Optimized collection/playlist filtering
|
||||||
|
* Added sort tags to collection/playlist filtering (#1966)
|
||||||
|
|
||||||
|
Version 1.2.16 (2025.12.16):
|
||||||
|
|
||||||
|
* Make Discord Rich presence use filename if song title is missing
|
||||||
|
* Added better error message when a GStreamer plugin is missing
|
||||||
|
* Preserve track order in album shuffle mode when restarting playback (#1623)
|
||||||
|
* Possible fixes for context word wrap
|
||||||
|
* Added lyrics from lrclib.net
|
||||||
|
* Added option to turn off the use of sort tags for the collection
|
||||||
|
* Fixed Spotify login
|
||||||
|
* Fixed error dialog shown minimized if another Strawberry window than the mainwindow was active
|
||||||
|
* Fixed seeking to the end of the track and back causing seeking to stop working (#1675)
|
||||||
|
* Set current index when automatically selecting track (#1825)
|
||||||
|
* Make icon size for shuffle and repeat buttons adjust to screen resolution (#1838)
|
||||||
|
* Fixed song being removed from playlist when dragging to another application (#1815)
|
||||||
|
* Don't automatically scroll on dynamic playlists (#1427)
|
||||||
|
|
||||||
Version 1.2.15 (2025.11.25):
|
Version 1.2.15 (2025.11.25):
|
||||||
|
|
||||||
* Fixed system default language not respected
|
* Fixed system default language not respected
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Supporting open-source developers helps ensure continued maintenance and improve
|
|||||||
- Loudness analysis and EBU R128 normalization
|
- Loudness analysis and EBU R128 normalization
|
||||||
- Editing tags and fetching missing tags via [MusicBrainz](https://musicbrainz.org/)
|
- 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/)
|
- 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
|
- Audio analyzer and equalizer
|
||||||
- Transfer music to USB, MTP and iPod devices
|
- Transfer music to USB, MTP and iPod devices
|
||||||
- Scrobbling to [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
- Scrobbling to [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||||
set(STRAWBERRY_VERSION_MINOR 2)
|
set(STRAWBERRY_VERSION_MINOR 2)
|
||||||
set(STRAWBERRY_VERSION_PATCH 15)
|
set(STRAWBERRY_VERSION_PATCH 17)
|
||||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||||
|
|
||||||
set(INCLUDE_GIT_REVISION OFF)
|
set(INCLUDE_GIT_REVISION OFF)
|
||||||
|
|||||||
2
debian/control
vendored
2
debian/control
vendored
@@ -60,7 +60,7 @@ Description: music player and music collection organizer
|
|||||||
- Edit tags on audio files
|
- Edit tags on audio files
|
||||||
- Automatically retrieve tags from MusicBrainz
|
- Automatically retrieve tags from MusicBrainz
|
||||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
- Lyrics from multiple sources
|
||||||
- Audio analyzer
|
- Audio analyzer
|
||||||
- Audio equalizer
|
- Audio equalizer
|
||||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<li>Edit tags on audio files</li>
|
<li>Edit tags on audio files</li>
|
||||||
<li>Automatically retrieve tags from MusicBrainz</li>
|
<li>Automatically retrieve tags from MusicBrainz</li>
|
||||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||||
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind</li>
|
<li>Lyrics from multiple sources</li>
|
||||||
<li>Audio analyzer and equalizer</li>
|
<li>Audio analyzer and equalizer</li>
|
||||||
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
||||||
<li>Scrobbler with support for Last.fm and ListenBrainz</li>
|
<li>Scrobbler with support for Last.fm and ListenBrainz</li>
|
||||||
@@ -51,6 +51,8 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="1.2.17" date="2026-01-18"/>
|
||||||
|
<release version="1.2.16" date="2025-12-16"/>
|
||||||
<release version="1.2.15" date="2025-11-25"/>
|
<release version="1.2.15" date="2025-11-25"/>
|
||||||
<release version="1.2.14" date="2025-10-25"/>
|
<release version="1.2.14" date="2025-10-25"/>
|
||||||
<release version="1.2.13" date="2025-08-31"/>
|
<release version="1.2.13" date="2025-08-31"/>
|
||||||
|
|||||||
4
dist/unix/strawberry.1
vendored
4
dist/unix/strawberry.1
vendored
@@ -29,9 +29,7 @@ Features:
|
|||||||
.br
|
.br
|
||||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||||
.br
|
.br
|
||||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
- Lyrics from multiple sources
|
||||||
.br
|
|
||||||
- Support for multiple backends
|
|
||||||
.br
|
.br
|
||||||
- Audio analyzer
|
- Audio analyzer
|
||||||
.br
|
.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
|
- Edit tags on audio files
|
||||||
- Automatically retrieve tags from MusicBrainz
|
- Automatically retrieve tags from MusicBrainz
|
||||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
- Lyrics from multiple sources
|
||||||
- Support for multiple backends
|
|
||||||
- Audio analyzer
|
- Audio analyzer
|
||||||
- Audio equalizer
|
- Audio equalizer
|
||||||
- Scrobbler with support for Last.fm and ListenBrainz
|
- Scrobbler with support for Last.fm and ListenBrainz
|
||||||
|
|||||||
@@ -90,4 +90,3 @@ class AnalyzerBase : public QWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // ANALYZERBASE_H
|
#endif // ANALYZERBASE_H
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ void AnalyzerContainer::AddAnalyzerType() {
|
|||||||
action->setCheckable(true);
|
action->setCheckable(true);
|
||||||
actions_ << action;
|
actions_ << action;
|
||||||
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); });
|
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // ANALYZERCONTAINER_H
|
#endif // ANALYZERCONTAINER_H
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ BlockAnalyzer::BlockAnalyzer(QWidget *parent)
|
|||||||
|
|
||||||
// mxcl says null pixmaps cause crashes, so let's play it safe
|
// mxcl says null pixmaps cause crashes, so let's play it safe
|
||||||
std::fill(fade_bars_.begin(), fade_bars_.end(), QPixmap(1, 1));
|
std::fill(fade_bars_.begin(), fade_bars_.end(), QPixmap(1, 1));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
|
void BlockAnalyzer::resizeEvent(QResizeEvent *e) {
|
||||||
|
|||||||
@@ -41,14 +41,14 @@ class BlockAnalyzer : public AnalyzerBase {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Q_INVOKABLE explicit BlockAnalyzer(QWidget*);
|
Q_INVOKABLE explicit BlockAnalyzer(QWidget *parent);
|
||||||
|
|
||||||
static const char *kName;
|
static const char *kName;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void transform(Scope&) override;
|
void transform(Scope &s) override;
|
||||||
void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
|
void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
|
||||||
void resizeEvent(QResizeEvent*) override;
|
void resizeEvent(QResizeEvent *e) override;
|
||||||
virtual void paletteChange(const QPalette &_palette);
|
virtual void paletteChange(const QPalette &_palette);
|
||||||
void framerateChanged() override;
|
void framerateChanged() override;
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class BoomAnalyzer : public AnalyzerBase {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Q_INVOKABLE explicit BoomAnalyzer(QWidget*);
|
Q_INVOKABLE explicit BoomAnalyzer(QWidget *parent);
|
||||||
|
|
||||||
static const char *kName;
|
static const char *kName;
|
||||||
|
|
||||||
@@ -70,7 +70,6 @@ class BoomAnalyzer : public AnalyzerBase {
|
|||||||
|
|
||||||
QPixmap barPixmap_;
|
QPixmap barPixmap_;
|
||||||
QPixmap canvas_;
|
QPixmap canvas_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BOOMANALYZER_H
|
#endif // BOOMANALYZER_H
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class FHT {
|
|||||||
/**
|
/**
|
||||||
* Recursive in-place Hartley transform. For internal use only!
|
* Recursive in-place Hartley transform. For internal use only!
|
||||||
*/
|
*/
|
||||||
void _transform(float*, int, int);
|
void _transform(float *p, int n, int k);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
@@ -68,7 +68,7 @@ class FHT {
|
|||||||
~FHT();
|
~FHT();
|
||||||
int sizeExp() const;
|
int sizeExp() const;
|
||||||
int size() const;
|
int size() const;
|
||||||
void scale(float*, float) const;
|
void scale(float *p, float d) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exponentially Weighted Moving Average (EWMA) filter.
|
* Exponentially Weighted Moving Average (EWMA) filter.
|
||||||
@@ -90,12 +90,12 @@ class FHT {
|
|||||||
/**
|
/**
|
||||||
* Semi-logarithmic audio spectrum.
|
* Semi-logarithmic audio spectrum.
|
||||||
*/
|
*/
|
||||||
void semiLogSpectrum(float*);
|
void semiLogSpectrum(float *p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fourier spectrum.
|
* Fourier spectrum.
|
||||||
*/
|
*/
|
||||||
void spectrum(float*);
|
void spectrum(float *p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates a mathematically correct FFT power spectrum.
|
* Calculates a mathematically correct FFT power spectrum.
|
||||||
@@ -103,7 +103,7 @@ class FHT {
|
|||||||
* and factor the 0.5 in the final scaling factor.
|
* and factor the 0.5 in the final scaling factor.
|
||||||
* @see FHT::power2()
|
* @see FHT::power2()
|
||||||
*/
|
*/
|
||||||
void power(float*);
|
void power(float *p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates an FFT power spectrum with doubled values as a
|
* Calculates an FFT power spectrum with doubled values as a
|
||||||
@@ -112,14 +112,14 @@ class FHT {
|
|||||||
* of @f$2^n@f$ input values. This is the fastest transform.
|
* of @f$2^n@f$ input values. This is the fastest transform.
|
||||||
* @see FHT::power()
|
* @see FHT::power()
|
||||||
*/
|
*/
|
||||||
void power2(float*);
|
void power2(float *p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discrete Hartley transform of data sets with 8 values.
|
* Discrete Hartley transform of data sets with 8 values.
|
||||||
*/
|
*/
|
||||||
static void transform8(float*);
|
static void transform8(float *p);
|
||||||
|
|
||||||
void transform(float*);
|
void transform(float *p);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // FHT_H
|
#endif // FHT_H
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -537,18 +537,6 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
|||||||
|
|
||||||
ScopedTransaction transaction(&db);
|
ScopedTransaction transaction(&db);
|
||||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||||
if (subdir.mtime == 0) {
|
|
||||||
// Delete the subdirectory
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// See if this subdirectory already exists in the database
|
// See if this subdirectory already exists in the database
|
||||||
bool exists = false;
|
bool exists = false;
|
||||||
{
|
{
|
||||||
@@ -586,6 +574,26 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -140,7 +140,6 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
Q_INVOKABLE explicit CollectionBackend(QObject *parent = nullptr);
|
Q_INVOKABLE explicit CollectionBackend(QObject *parent = nullptr);
|
||||||
|
|
||||||
~CollectionBackend();
|
~CollectionBackend();
|
||||||
@@ -253,6 +252,7 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||||||
void DeleteSongsByUrls(const QList<QUrl> &url);
|
void DeleteSongsByUrls(const QList<QUrl> &url);
|
||||||
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
|
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
|
||||||
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
|
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||||
|
void DeleteSubdirs(const CollectionSubdirectoryList &subdirs);
|
||||||
void CompilationsNeedUpdating();
|
void CompilationsNeedUpdating();
|
||||||
void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded);
|
void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded);
|
||||||
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual);
|
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual);
|
||||||
@@ -331,4 +331,3 @@ class CollectionBackend : public CollectionBackendInterface {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // COLLECTIONBACKEND_H
|
#endif // COLLECTIONBACKEND_H
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
|
|
||||||
class CollectionFilterOptions {
|
class CollectionFilterOptions {
|
||||||
public:
|
public:
|
||||||
|
|
||||||
explicit CollectionFilterOptions();
|
explicit CollectionFilterOptions();
|
||||||
|
|
||||||
// Filter mode:
|
// Filter mode:
|
||||||
|
|||||||
@@ -135,4 +135,3 @@ class CollectionFilterWidget : public QWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // COLLECTIONFILTERWIDGET_H
|
#endif // COLLECTIONFILTERWIDGET_H
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -124,6 +124,7 @@ void CollectionLibrary::Init() {
|
|||||||
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
|
QObject::connect(watcher_, &CollectionWatcher::SongsReadded, &*backend_, &CollectionBackend::MarkSongsUnavailable);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
QObject::connect(watcher_, &CollectionWatcher::SubdirsDiscovered, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
QObject::connect(watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||||
|
QObject::connect(watcher_, &CollectionWatcher::SubdirsDeleted, &*backend_, &CollectionBackend::DeleteSubdirs);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
|
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
|
||||||
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);
|
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);
|
||||||
|
|
||||||
@@ -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 CollectionLibrary::SyncPlaycountAndRatingToFilesAsync() {
|
||||||
|
|
||||||
(void)QtConcurrent::run(&CollectionLibrary::SyncPlaycountAndRatingToFiles, this);
|
(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_) {
|
if (save_tags || save_playcounts_to_files_) {
|
||||||
tagreader_client_->SaveSongsPlaycountAsync(songs);
|
SongList songs_to_save_now;
|
||||||
|
for (const Song &song : songs) {
|
||||||
|
if (song.url().isLocalFile() && song.url() == current_song_url_ &&
|
||||||
|
(song.filetype() == Song::FileType::OggFlac || song.filetype() == Song::FileType::OggVorbis || song.filetype() == Song::FileType::OggOpus)) {
|
||||||
|
qLog(Debug) << "Deferring playcount save for currently playing file" << song.url().toLocalFile();
|
||||||
|
if (pending_song_saves_.contains(song.url())) {
|
||||||
|
SharedPtr<PendingSongSave> pending_song_save = pending_song_saves_[song.url()];
|
||||||
|
pending_song_save->save_playcount = true;
|
||||||
|
pending_song_save->song.set_playcount(song.playcount());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SharedPtr<PendingSongSave> pending_song_save = make_shared<PendingSongSave>();
|
||||||
|
pending_song_save->save_playcount = true;
|
||||||
|
pending_song_save->song = song;
|
||||||
|
pending_song_saves_.insert(song.url(), pending_song_save);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
songs_to_save_now << song;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!songs_to_save_now.isEmpty()) {
|
||||||
|
tagreader_client_->SaveSongsPlaycountAsync(songs_to_save_now);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CollectionLibrary::SongsRatingChanged(const SongList &songs, const bool save_tags) const {
|
void CollectionLibrary::SongsRatingChanged(const SongList &songs, const bool save_tags) {
|
||||||
|
|
||||||
if (save_tags || save_ratings_to_files_) {
|
if (save_tags || save_ratings_to_files_) {
|
||||||
tagreader_client_->SaveSongsRatingAsync(songs);
|
SongList songs_to_save_now;
|
||||||
|
for (const Song &song : songs) {
|
||||||
|
if (song.url().isLocalFile() && song.url() == current_song_url_ &&
|
||||||
|
(song.filetype() == Song::FileType::OggFlac || song.filetype() == Song::FileType::OggVorbis || song.filetype() == Song::FileType::OggOpus)) {
|
||||||
|
qLog(Debug) << "Deferring rating save for currently playing file" << song.url().toLocalFile();
|
||||||
|
if (pending_song_saves_.contains(song.url())) {
|
||||||
|
SharedPtr<PendingSongSave> pending_song_save = pending_song_saves_[song.url()];
|
||||||
|
pending_song_save->save_rating = true;
|
||||||
|
pending_song_save->song.set_rating(song.rating());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SharedPtr<PendingSongSave> pending_song_save = make_shared<PendingSongSave>();
|
||||||
|
pending_song_save->save_rating = true;
|
||||||
|
pending_song_save->song = song;
|
||||||
|
pending_song_saves_.insert(song.url(), pending_song_save);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
songs_to_save_now << song;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!songs_to_save_now.isEmpty()) {
|
||||||
|
tagreader_client_->SaveSongsRatingAsync(songs_to_save_now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void CollectionLibrary::SavePendingPlaycountsAndRatings() {
|
||||||
|
|
||||||
|
for (auto it = pending_song_saves_.constBegin(); it != pending_song_saves_.constEnd();) {
|
||||||
|
const QUrl url = it.key();
|
||||||
|
SharedPtr<PendingSongSave> pending_song_save = it.value();
|
||||||
|
if (url == current_song_url_) {
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
qLog(Debug) << "Saving deferred playcount/rating for" << url.toLocalFile();
|
||||||
|
if (pending_song_save->save_playcount) {
|
||||||
|
tagreader_client_->SaveSongsPlaycountAsync(SongList() << pending_song_save->song);
|
||||||
|
}
|
||||||
|
if (pending_song_save->save_rating) {
|
||||||
|
tagreader_client_->SaveSongsRatingAsync(SongList() << pending_song_save->song);
|
||||||
|
}
|
||||||
|
it = pending_song_saves_.erase(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
#include <QMap>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "includes/shared_ptr.h"
|
#include "includes/shared_ptr.h"
|
||||||
@@ -71,6 +72,7 @@ class CollectionLibrary : public QObject {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void SyncPlaycountAndRatingToFiles();
|
void SyncPlaycountAndRatingToFiles();
|
||||||
|
void SavePendingPlaycountsAndRatings();
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void ReloadSettings();
|
void ReloadSettings();
|
||||||
@@ -84,16 +86,26 @@ class CollectionLibrary : public QObject {
|
|||||||
|
|
||||||
void IncrementalScan();
|
void IncrementalScan();
|
||||||
|
|
||||||
|
void CurrentSongChanged(const Song &song);
|
||||||
|
void Stopped();
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void ExitReceived();
|
void ExitReceived();
|
||||||
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false) const;
|
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false);
|
||||||
void SongsRatingChanged(const SongList &songs, const bool save_tags = false) const;
|
void SongsRatingChanged(const SongList &songs, const bool save_tags = false);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void Error(const QString &error);
|
void Error(const QString &error);
|
||||||
void ExitFinished();
|
void ExitFinished();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
class PendingSongSave {
|
||||||
|
public:
|
||||||
|
Song song;
|
||||||
|
bool save_playcount = false;
|
||||||
|
bool save_rating = false;
|
||||||
|
};
|
||||||
|
|
||||||
const SharedPtr<TaskManager> task_manager_;
|
const SharedPtr<TaskManager> task_manager_;
|
||||||
const SharedPtr<TagReaderClient> tagreader_client_;
|
const SharedPtr<TagReaderClient> tagreader_client_;
|
||||||
|
|
||||||
@@ -111,6 +123,10 @@ class CollectionLibrary : public QObject {
|
|||||||
|
|
||||||
bool save_playcounts_to_files_;
|
bool save_playcounts_to_files_;
|
||||||
bool save_ratings_to_files_;
|
bool save_ratings_to_files_;
|
||||||
|
|
||||||
|
QUrl current_song_url_;
|
||||||
|
|
||||||
|
QMap<QUrl, SharedPtr<PendingSongSave>> pending_song_saves_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ void CollectionModel::ReloadSettings() {
|
|||||||
const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool();
|
const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool();
|
||||||
const bool sort_skip_articles_for_artists = settings.value(CollectionSettings::kSkipArticlesForArtists, 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 sort_skip_articles_for_albums = settings.value(CollectionSettings::kSkipArticlesForAlbums, false).toBool();
|
||||||
|
const bool use_sort_tags = settings.value(CollectionSettings::kUseSortTags, true).toBool();
|
||||||
|
|
||||||
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
|
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
|
||||||
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
|
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
|
||||||
@@ -227,12 +228,14 @@ void CollectionModel::ReloadSettings() {
|
|||||||
show_dividers != options_current_.show_dividers ||
|
show_dividers != options_current_.show_dividers ||
|
||||||
show_various_artists != options_current_.show_various_artists ||
|
show_various_artists != options_current_.show_various_artists ||
|
||||||
sort_skip_articles_for_artists != options_current_.sort_skip_articles_for_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_pretty_covers = show_pretty_covers;
|
||||||
options_current_.show_dividers = show_dividers;
|
options_current_.show_dividers = show_dividers;
|
||||||
options_current_.show_various_artists = show_various_artists;
|
options_current_.show_various_artists = show_various_artists;
|
||||||
options_current_.sort_skip_articles_for_artists = sort_skip_articles_for_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_.sort_skip_articles_for_albums = sort_skip_articles_for_albums;
|
||||||
|
options_current_.use_sort_tags = use_sort_tags;
|
||||||
ScheduleReset();
|
ScheduleReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,7 +708,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
|||||||
|
|
||||||
QString divider_key;
|
QString divider_key;
|
||||||
if (options_active_.show_dividers && container_level == 0) {
|
if (options_active_.show_dividers && container_level == 0) {
|
||||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_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_key.isEmpty()) {
|
||||||
if (!divider_nodes_.contains(divider_key)) {
|
if (!divider_nodes_.contains(divider_key)) {
|
||||||
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
|
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
|
||||||
@@ -719,7 +722,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
|||||||
item->container_level = container_level;
|
item->container_level = container_level;
|
||||||
item->container_key = container_key;
|
item->container_key = container_key;
|
||||||
item->display_text = DisplayText(group_by, song);
|
item->display_text = DisplayText(group_by, song);
|
||||||
item->sort_text = SortText(group_by, song, options_active_.sort_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()) {
|
if (!divider_key.isEmpty()) {
|
||||||
item->sort_text.prepend(divider_key + QLatin1Char(' '));
|
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) {
|
switch (group_by) {
|
||||||
case GroupBy::AlbumArtist:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
case GroupBy::Disc:
|
||||||
return SortTextForNumber(std::max(0, song.disc()));
|
return SortTextForNumber(std::max(0, song.disc()));
|
||||||
case GroupBy::Year:
|
case GroupBy::Year:
|
||||||
return SortTextForNumber(std::max(0, song.year())) + QLatin1Char(' ');
|
return SortTextForYear(song.year()) + QLatin1Char(' ');
|
||||||
case GroupBy::OriginalYear:
|
case GroupBy::OriginalYear:
|
||||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
|
return SortTextForYear(song.effective_originalyear()) + QLatin1Char(' ');
|
||||||
case GroupBy::Genre:
|
case GroupBy::Genre:
|
||||||
return SortText(song.genre());
|
return SortText(song.genre());
|
||||||
case GroupBy::Composer:
|
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:
|
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:
|
case GroupBy::Grouping:
|
||||||
return SortText(song.grouping());
|
return SortText(song.grouping());
|
||||||
case GroupBy::FileType:
|
case GroupBy::FileType:
|
||||||
@@ -1162,14 +1165,14 @@ QString CollectionModel::SortTextForSong(const Song &song) {
|
|||||||
|
|
||||||
QString CollectionModel::SortTextForYear(const int year) {
|
QString CollectionModel::SortTextForYear(const int year) {
|
||||||
|
|
||||||
QString str = QString::number(year);
|
const QString str = QString::number(std::max(year, 0));
|
||||||
return QStringLiteral("0").repeated(qMax(0, 4 - str.length())) + str;
|
return QStringLiteral("0").repeated(qMax(0, 4 - str.length())) + str;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString CollectionModel::SortTextForBitrate(const int bitrate) {
|
QString CollectionModel::SortTextForBitrate(const int bitrate) {
|
||||||
|
|
||||||
QString str = QString::number(bitrate);
|
const QString str = QString::number(bitrate);
|
||||||
return QStringLiteral("0").repeated(qMax(0, 3 - str.length())) + str;
|
return QStringLiteral("0").repeated(qMax(0, 3 - str.length())) + str;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
case GroupBy::AlbumDisc:
|
case GroupBy::AlbumDisc:
|
||||||
key = PrettyAlbumDisc(song.album(), song.disc());
|
key = TextOrUnknown(song.album());
|
||||||
|
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
|
||||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
case GroupBy::YearAlbum:
|
case GroupBy::YearAlbum:
|
||||||
key = PrettyYearAlbum(song.year(), song.album());
|
key = SortTextForYear(song.year()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
case GroupBy::YearAlbumDisc:
|
case GroupBy::YearAlbumDisc:
|
||||||
key = PrettyYearAlbumDisc(song.year(), song.album(), song.disc());
|
key = SortTextForYear(song.year()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||||
|
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
|
||||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
case GroupBy::OriginalYearAlbum:
|
case GroupBy::OriginalYearAlbum:
|
||||||
key = PrettyYearAlbum(song.effective_originalyear(), song.album());
|
key = SortTextForYear(song.effective_originalyear()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
case GroupBy::OriginalYearAlbumDisc:
|
case GroupBy::OriginalYearAlbumDisc:
|
||||||
key = PrettyYearAlbumDisc(song.effective_originalyear(), song.album(), song.disc());
|
key = SortTextForYear(song.effective_originalyear()) + QLatin1Char('-') + TextOrUnknown(song.album());
|
||||||
|
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
|
||||||
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
|
||||||
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
|
||||||
break;
|
break;
|
||||||
@@ -1246,10 +1252,10 @@ QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song,
|
|||||||
key = PrettyDisc(song.disc());
|
key = PrettyDisc(song.disc());
|
||||||
break;
|
break;
|
||||||
case GroupBy::Year:
|
case GroupBy::Year:
|
||||||
key = QString::number(std::max(0, song.year()));
|
key = SortTextForYear(song.year());
|
||||||
break;
|
break;
|
||||||
case GroupBy::OriginalYear:
|
case GroupBy::OriginalYear:
|
||||||
key = QString::number(std::max(0, song.effective_originalyear()));
|
key = SortTextForYear(song.effective_originalyear());
|
||||||
break;
|
break;
|
||||||
case GroupBy::Genre:
|
case GroupBy::Genre:
|
||||||
key = TextOrUnknown(song.genre());
|
key = TextOrUnknown(song.genre());
|
||||||
@@ -1341,7 +1347,7 @@ QString CollectionModel::DividerKey(const GroupBy group_by, const Song &song, co
|
|||||||
case GroupBy::Bitdepth:
|
case GroupBy::Bitdepth:
|
||||||
return SortTextForNumber(song.bitdepth());
|
return SortTextForNumber(song.bitdepth());
|
||||||
case GroupBy::Bitrate:
|
case GroupBy::Bitrate:
|
||||||
return SortTextForNumber(song.bitrate());
|
return SortTextForBitrate(song.bitrate());
|
||||||
case GroupBy::None:
|
case GroupBy::None:
|
||||||
case GroupBy::GroupByCount:
|
case GroupBy::GroupByCount:
|
||||||
return QString();
|
return QString();
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||||||
show_various_artists(true),
|
show_various_artists(true),
|
||||||
sort_skip_articles_for_artists(false),
|
sort_skip_articles_for_artists(false),
|
||||||
sort_skip_articles_for_albums(false),
|
sort_skip_articles_for_albums(false),
|
||||||
|
use_sort_tags(true),
|
||||||
separate_albums_by_grouping(false) {}
|
separate_albums_by_grouping(false) {}
|
||||||
|
|
||||||
Grouping group_by;
|
Grouping group_by;
|
||||||
@@ -139,6 +140,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||||||
bool show_various_artists;
|
bool show_various_artists;
|
||||||
bool sort_skip_articles_for_artists;
|
bool sort_skip_articles_for_artists;
|
||||||
bool sort_skip_articles_for_albums;
|
bool sort_skip_articles_for_albums;
|
||||||
|
bool use_sort_tags;
|
||||||
bool separate_albums_by_grouping;
|
bool separate_albums_by_grouping;
|
||||||
CollectionFilterOptions filter_options;
|
CollectionFilterOptions filter_options;
|
||||||
};
|
};
|
||||||
@@ -178,14 +180,14 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
|||||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||||
|
|
||||||
// Utility functions for manipulating text
|
// Utility functions for manipulating text
|
||||||
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 TextOrUnknown(const QString &text);
|
||||||
static QString PrettyYearAlbum(const int year, const QString &album);
|
static QString PrettyYearAlbum(const int year, const QString &album);
|
||||||
static QString PrettyAlbumDisc(const QString &album, const int disc);
|
static QString PrettyAlbumDisc(const QString &album, const int disc);
|
||||||
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
||||||
static QString PrettyDisc(const int disc);
|
static QString PrettyDisc(const int disc);
|
||||||
static QString PrettyFormat(const Song &song);
|
static QString PrettyFormat(const Song &song);
|
||||||
static 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 SortText(QString text);
|
||||||
static QString SortTextForName(const QString &name, const bool sort_skip_articles);
|
static QString SortTextForName(const QString &name, const bool sort_skip_articles);
|
||||||
static QString SortTextForNumber(const int number);
|
static QString SortTextForNumber(const int number);
|
||||||
|
|||||||
@@ -58,4 +58,3 @@ class CollectionPlaylistItem : public PlaylistItem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // COLLECTIONPLAYLISTITEM_H
|
#endif // COLLECTIONPLAYLISTITEM_H
|
||||||
|
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ void CollectionView::keyPressEvent(QKeyEvent *e) {
|
|||||||
case Qt::Key_Enter:
|
case Qt::Key_Enter:
|
||||||
case Qt::Key_Return:
|
case Qt::Key_Return:
|
||||||
if (currentIndex().isValid()) {
|
if (currentIndex().isValid()) {
|
||||||
AddToPlaylist();
|
Q_EMIT doubleClicked(currentIndex());
|
||||||
}
|
}
|
||||||
e->accept();
|
e->accept();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -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)
|
CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher, const int dir, const bool incremental, const bool ignores_mtime, const bool mark_songs_unavailable)
|
||||||
: progress_(0),
|
: progress_(0),
|
||||||
progress_max_(0),
|
progress_max_(0),
|
||||||
dir_(dir),
|
dir_id_(dir),
|
||||||
incremental_(incremental),
|
incremental_(incremental),
|
||||||
ignores_mtime_(ignores_mtime),
|
ignores_mtime_(ignores_mtime),
|
||||||
mark_songs_unavailable_(mark_songs_unavailable),
|
mark_songs_unavailable_(mark_songs_unavailable),
|
||||||
@@ -313,6 +313,19 @@ void CollectionWatcher::ScanTransaction::AddToProgressMax(const quint64 n) {
|
|||||||
|
|
||||||
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
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 (!deleted_songs.isEmpty()) {
|
||||||
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
|
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
|
||||||
Q_EMIT watcher_->SongsUnavailable(deleted_songs);
|
Q_EMIT watcher_->SongsUnavailable(deleted_songs);
|
||||||
@@ -338,34 +351,24 @@ void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
|
|||||||
readded_songs.clear();
|
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)) {
|
for (const CollectionSubdirectory &subdir : std::as_const(deleted_subdirs)) {
|
||||||
if (watcher_->watched_dirs_.contains(dir_)) {
|
if (watcher_->watched_dirs_.contains(dir_id_)) {
|
||||||
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_], subdir);
|
watcher_->RemoveWatch(watcher_->watched_dirs_[dir_id_], subdir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deleted_subdirs.clear();
|
deleted_subdirs.clear();
|
||||||
|
|
||||||
if (watcher_->monitor_) {
|
if (watcher_->monitor_) {
|
||||||
// Watch the new subdirectories
|
|
||||||
for (const CollectionSubdirectory &subdir : std::as_const(new_subdirs)) {
|
for (const CollectionSubdirectory &subdir : std::as_const(new_subdirs)) {
|
||||||
if (watcher_->watched_dirs_.contains(dir_)) {
|
if (watcher_->watched_dirs_.contains(dir_id_)) {
|
||||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
|
watcher_->AddWatch(watcher_->watched_dirs_[dir_id_], subdir.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
new_subdirs.clear();
|
new_subdirs.clear();
|
||||||
|
|
||||||
if (incremental_ || ignores_mtime_) {
|
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) {
|
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
|
||||||
|
|
||||||
if (cached_songs_dirty_) {
|
if (cached_songs_dirty_) {
|
||||||
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_);
|
const SongList songs = watcher_->backend_->FindSongsInDirectory(dir_id_);
|
||||||
for (const Song &song : songs) {
|
for (const Song &song : songs) {
|
||||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||||
cached_songs_.insert(p, song);
|
cached_songs_.insert(p, song);
|
||||||
@@ -393,7 +396,7 @@ SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QStri
|
|||||||
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QString &path) {
|
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QString &path) {
|
||||||
|
|
||||||
if (cached_songs_missing_fingerprint_dirty_) {
|
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) {
|
for (const Song &song : songs) {
|
||||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||||
cached_songs_missing_fingerprint_.insert(p, song);
|
cached_songs_missing_fingerprint_.insert(p, song);
|
||||||
@@ -408,7 +411,7 @@ bool CollectionWatcher::ScanTransaction::HasSongsWithMissingFingerprint(const QS
|
|||||||
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingLoudnessCharacteristics(const QString &path) {
|
bool CollectionWatcher::ScanTransaction::HasSongsWithMissingLoudnessCharacteristics(const QString &path) {
|
||||||
|
|
||||||
if (cached_songs_missing_loudness_characteristics_dirty_) {
|
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) {
|
for (const Song &song : songs) {
|
||||||
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
const QString p = song.url().toLocalFile().section(u'/', 0, -2);
|
||||||
cached_songs_missing_loudness_characteristics_.insert(p, song);
|
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) {
|
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||||
|
|
||||||
if (known_subdirs_dirty_) {
|
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; });
|
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) {
|
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
|
||||||
|
|
||||||
if (known_subdirs_dirty_) {
|
if (known_subdirs_dirty_) {
|
||||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionSubdirectoryList ret;
|
CollectionSubdirectoryList ret;
|
||||||
@@ -457,7 +460,7 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdi
|
|||||||
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
||||||
|
|
||||||
if (known_subdirs_dirty_) {
|
if (known_subdirs_dirty_) {
|
||||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_id_));
|
||||||
}
|
}
|
||||||
|
|
||||||
return known_subdirs_;
|
return known_subdirs_;
|
||||||
@@ -494,7 +497,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
|||||||
const quint64 files_count = FilesCountForPath(&transaction, dir.path);
|
const quint64 files_count = FilesCountForPath(&transaction, dir.path);
|
||||||
transaction.SetKnownSubdirs(subdirs);
|
transaction.SetKnownSubdirs(subdirs);
|
||||||
transaction.AddToProgressMax(files_count);
|
transaction.AddToProgressMax(files_count);
|
||||||
ScanSubdirectory(dir.path, CollectionSubdirectory(), files_count, &transaction);
|
ScanSubdirectory(dir, dir.path, CollectionSubdirectory(), files_count, &transaction);
|
||||||
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -512,7 +515,7 @@ void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const Colle
|
|||||||
transaction.AddToProgressMax(files_count);
|
transaction.AddToProgressMax(files_count);
|
||||||
for (const CollectionSubdirectory &subdir : subdirs) {
|
for (const CollectionSubdirectory &subdir : subdirs) {
|
||||||
if (stop_or_abort_requested()) break;
|
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()) {
|
if (!stop_or_abort_requested()) {
|
||||||
last_scan_time_ = QDateTime::currentSecsSinceEpoch();
|
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 QFileInfo path_info(path);
|
||||||
|
const qint64 path_mtime = path_info.exists() && path_info.lastModified().isValid() ? path_info.lastModified().toSecsSinceEpoch() : 0;
|
||||||
|
|
||||||
if (path_info.isSymLink()) {
|
if (path_info.isSymLink()) {
|
||||||
const QString real_path = path_info.symLinkTarget();
|
const QString real_path = path_info.symLinkTarget();
|
||||||
@@ -536,8 +540,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Do not scan symlinked dirs that are already in collection
|
// Do not scan symlinked dirs that are already in collection
|
||||||
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
|
for (const CollectionDirectory &i : std::as_const(watched_dirs_)) {
|
||||||
if (real_path.startsWith(dir.path)) {
|
if (real_path.startsWith(i.path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,7 +567,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
|||||||
}
|
}
|
||||||
#endif
|
#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
|
// The directory hasn't changed since last time
|
||||||
t->AddToProgress(files_count);
|
t->AddToProgress(files_count);
|
||||||
return;
|
return;
|
||||||
@@ -578,11 +582,12 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
|||||||
const CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
const CollectionSubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
||||||
for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
|
for (const CollectionSubdirectory &prev_subdir : previous_subdirs) {
|
||||||
if (!QFile::exists(prev_subdir.path) && prev_subdir.path != path) {
|
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.
|
// 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.
|
||||||
|
if (path_info.exists()) {
|
||||||
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
|
|
||||||
@@ -592,7 +597,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
|||||||
const QFileInfo child_fileinfo(child_filepath);
|
const QFileInfo child_fileinfo(child_filepath);
|
||||||
|
|
||||||
if (child_fileinfo.isSymLink()) {
|
if (child_fileinfo.isSymLink()) {
|
||||||
QStorageInfo storage_info(child_fileinfo.symLinkTarget());
|
const QStorageInfo storage_info(child_fileinfo.symLinkTarget());
|
||||||
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
|
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();
|
qLog(Warning) << "Ignoring symbolic link" << child_filepath << "which links to" << child_fileinfo.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
|
||||||
continue;
|
continue;
|
||||||
@@ -605,14 +610,14 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
|||||||
CollectionSubdirectory new_subdir;
|
CollectionSubdirectory new_subdir;
|
||||||
new_subdir.directory_id = -1;
|
new_subdir.directory_id = -1;
|
||||||
new_subdir.path = child_filepath;
|
new_subdir.path = child_filepath;
|
||||||
new_subdir.mtime = child_fileinfo.lastModified().toSecsSinceEpoch();
|
new_subdir.mtime = child_fileinfo.exists() && child_fileinfo.lastModified().isValid() ? child_fileinfo.lastModified().toSecsSinceEpoch() : 0;
|
||||||
my_new_subdirs << new_subdir;
|
my_new_subdirs << new_subdir;
|
||||||
}
|
}
|
||||||
t->AddToProgress(1);
|
t->AddToProgress(1);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
QString ext_part(ExtensionPart(child_filepath));
|
const QString ext_part = ExtensionPart(child_filepath);
|
||||||
QString dir_part(DirectoryPart(child_filepath));
|
const QString dir_part = DirectoryPart(child_filepath);
|
||||||
if (Song::kRejectedExtensions.contains(child_fileinfo.suffix(), Qt::CaseInsensitive) || child_fileinfo.baseName() == "qt_temp"_L1) {
|
if (Song::kRejectedExtensions.contains(child_fileinfo.suffix(), Qt::CaseInsensitive) || child_fileinfo.baseName() == "qt_temp"_L1) {
|
||||||
t->AddToProgress(1);
|
t->AddToProgress(1);
|
||||||
}
|
}
|
||||||
@@ -620,11 +625,9 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
|||||||
album_art[dir_part] << child_filepath;
|
album_art[dir_part] << child_filepath;
|
||||||
t->AddToProgress(1);
|
t->AddToProgress(1);
|
||||||
}
|
}
|
||||||
else if (tagreader_client_->IsMediaFileBlocking(child_filepath)) {
|
else {
|
||||||
files_on_disk << child_filepath;
|
files_on_disk << child_filepath;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
t->AddToProgress(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,27 +635,27 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
|||||||
if (stop_or_abort_requested()) return;
|
if (stop_or_abort_requested()) return;
|
||||||
|
|
||||||
// Ask the database for a list of files in this directory
|
// 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;
|
QSet<QString> cues_processed;
|
||||||
|
|
||||||
// Now compare the list from the database with the list of files on disk
|
// 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) {
|
for (const QString &file : files_on_disk_copy) {
|
||||||
|
|
||||||
if (stop_or_abort_requested()) return;
|
if (stop_or_abort_requested()) return;
|
||||||
|
|
||||||
// Associated CUE
|
// Associated CUE
|
||||||
QString new_cue = CueParser::FindCueFilename(file);
|
const QString new_cue = CueParser::FindCueFilename(file);
|
||||||
|
|
||||||
SongList matching_songs;
|
SongList matching_songs;
|
||||||
if (FindSongsByPath(songs_in_db, file, &matching_songs)) { // Found matching song in DB by path.
|
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.
|
// The song is in the database and still on disk.
|
||||||
// Check the mtime to see if it's been changed since it was added.
|
// Check the mtime to see if it's been changed since it was added.
|
||||||
QFileInfo fileinfo(file);
|
const QFileInfo fileinfo(file);
|
||||||
|
|
||||||
if (!fileinfo.exists()) {
|
if (!fileinfo.exists()) {
|
||||||
// Partially fixes race condition - if file was removed between being added to the list and now.
|
// 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.";
|
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.
|
// 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;
|
QString fingerprint;
|
||||||
#ifdef HAVE_SONGFINGERPRINTING
|
#ifdef HAVE_SONGFINGERPRINTING
|
||||||
@@ -721,19 +730,15 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
|
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.
|
else { // If CUE associated.
|
||||||
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
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.
|
else { // Search the DB by fingerprint.
|
||||||
QString 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.
|
// The song is in the database and still on disk.
|
||||||
// Check the mtime to see if it's been changed since it was added.
|
// Check the mtime to see if it's been changed since it was added.
|
||||||
QFileInfo fileinfo(file);
|
const QFileInfo fileinfo(file);
|
||||||
if (!fileinfo.exists()) {
|
if (!fileinfo.exists()) {
|
||||||
// Partially fixes race condition - if file was removed between being added to the list and now.
|
// Partially fixes race condition - if file was removed between being added to the list and now.
|
||||||
files_on_disk.removeAll(file);
|
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.
|
// Make sure the songs aren't deleted, as they still exist elsewhere with a different file path.
|
||||||
bool matching_songs_has_cue = false;
|
bool matching_songs_has_cue = false;
|
||||||
for (const Song &matching_song : std::as_const(matching_songs)) {
|
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)) {
|
if (!t->files_changed_path_.contains(matching_filename)) {
|
||||||
t->files_changed_path_ << matching_filename;
|
t->files_changed_path_ << matching_filename;
|
||||||
qLog(Debug) << matching_filename << "has changed path to" << file;
|
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);
|
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.
|
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.
|
else { // If CUE associated.
|
||||||
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
|
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);
|
const SongList songs = ScanNewFile(file, path, fingerprint, new_cue, &cues_processed);
|
||||||
if (songs.isEmpty()) {
|
if (songs.isEmpty()) {
|
||||||
|
files_on_disk.removeAll(file);
|
||||||
t->AddToProgress(1);
|
t->AddToProgress(1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -805,7 +813,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
|
|||||||
const QUrl art_automatic = ArtForSong(file, album_art);
|
const QUrl art_automatic = ArtForSong(file, album_art);
|
||||||
|
|
||||||
for (Song song : songs) {
|
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);
|
if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic);
|
||||||
t->new_songs << song;
|
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;
|
CollectionSubdirectory updated_subdir;
|
||||||
updated_subdir.directory_id = t->dir();
|
updated_subdir.directory_id = t->dir_id();
|
||||||
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toSecsSinceEpoch() : 0;
|
updated_subdir.mtime = path_mtime;
|
||||||
updated_subdir.path = path;
|
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;
|
t->new_subdirs << updated_subdir;
|
||||||
}
|
}
|
||||||
else {
|
else if (subdir.mtime != updated_subdir.mtime) {
|
||||||
t->touched_subdirs << updated_subdir;
|
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
|
// Recurse into the new subdirs that we found
|
||||||
for (const CollectionSubdirectory &my_new_subdir : std::as_const(my_new_subdirs)) {
|
for (const CollectionSubdirectory &my_new_subdir : std::as_const(my_new_subdirs)) {
|
||||||
if (stop_or_abort_requested()) return;
|
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;
|
QSet<int> used_ids;
|
||||||
for (Song new_cue_song : songs) {
|
for (Song new_cue_song : songs) {
|
||||||
new_cue_song.set_source(source_);
|
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);
|
PerformEBUR128Analysis(new_cue_song);
|
||||||
new_cue_song.set_fingerprint(fingerprint);
|
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 QString &fingerprint,
|
||||||
const SongList &matching_songs,
|
const SongList &matching_songs,
|
||||||
const QUrl &art_automatic,
|
const QUrl &art_automatic,
|
||||||
@@ -922,7 +929,7 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
|||||||
const TagReaderResult result = tagreader_client_->ReadFileBlocking(file, &song_on_disk);
|
const TagReaderResult result = tagreader_client_->ReadFileBlocking(file, &song_on_disk);
|
||||||
if (result.success() && song_on_disk.is_valid()) {
|
if (result.success() && song_on_disk.is_valid()) {
|
||||||
song_on_disk.set_source(source_);
|
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());
|
song_on_disk.set_id(matching_song.id());
|
||||||
PerformEBUR128Analysis(song_on_disk);
|
PerformEBUR128Analysis(song_on_disk);
|
||||||
song_on_disk.set_fingerprint(fingerprint);
|
song_on_disk.set_fingerprint(fingerprint);
|
||||||
@@ -931,6 +938,8 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
|
|||||||
AddChangedSong(file, matching_song, song_on_disk, t);
|
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 {
|
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() {
|
void CollectionWatcher::RescanPathsNow() {
|
||||||
|
|
||||||
const QList<int> dirs = rescan_queue_.keys();
|
const QList<int> dir_ids = rescan_queue_.keys();
|
||||||
for (const int dir : dirs) {
|
for (const int dir_id : dir_ids) {
|
||||||
if (stop_or_abort_requested()) break;
|
|
||||||
ScanTransaction transaction(this, dir, false, false, mark_songs_unavailable_);
|
|
||||||
|
|
||||||
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;
|
QMap<QString, quint64> subdir_files_count;
|
||||||
for (const QString &path : paths) {
|
for (const QString &path : paths) {
|
||||||
@@ -1215,11 +1225,14 @@ void CollectionWatcher::RescanPathsNow() {
|
|||||||
|
|
||||||
for (const QString &path : paths) {
|
for (const QString &path : paths) {
|
||||||
if (stop_or_abort_requested()) break;
|
if (stop_or_abort_requested()) break;
|
||||||
|
if (!subdir_mapping_.contains(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
CollectionSubdirectory subdir;
|
CollectionSubdirectory subdir;
|
||||||
subdir.directory_id = dir;
|
subdir.directory_id = dir_id;
|
||||||
subdir.mtime = 0;
|
subdir.mtime = 0;
|
||||||
subdir.path = path;
|
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_);
|
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes, mark_songs_unavailable_);
|
||||||
CollectionSubdirectoryList subdirs = transaction.GetAllSubdirs();
|
CollectionSubdirectoryList subdirs = transaction.GetAllSubdirs();
|
||||||
|
|
||||||
if (subdirs.isEmpty()) {
|
const bool has_collection_root_dir = std::any_of(subdirs.begin(), subdirs.end(), [&dir](const CollectionSubdirectory &subdir) { return subdir.path == dir.path; });
|
||||||
qLog(Debug) << "Collection directory wasn't in subdir list.";
|
if (!has_collection_root_dir) {
|
||||||
|
qLog(Debug) << "Collection directory wasn't in subdir list, re-adding";
|
||||||
CollectionSubdirectory subdir;
|
CollectionSubdirectory subdir;
|
||||||
subdir.path = dir.path;
|
|
||||||
subdir.directory_id = dir.id;
|
subdir.directory_id = dir.id;
|
||||||
|
subdir.path = dir.path;
|
||||||
|
subdir.mtime = 0;
|
||||||
subdirs << subdir;
|
subdirs << subdir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1358,7 +1373,7 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
|
|||||||
|
|
||||||
for (const CollectionSubdirectory &subdir : std::as_const(subdirs)) {
|
for (const CollectionSubdirectory &subdir : std::as_const(subdirs)) {
|
||||||
if (stop_or_abort_requested()) break;
|
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;
|
QStringList scanned_paths;
|
||||||
for (const Song &song : songs) {
|
for (const Song &song : songs) {
|
||||||
if (stop_or_abort_requested()) break;
|
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);
|
const QString song_path = song.url().toLocalFile().section(u'/', 0, -2);
|
||||||
if (scanned_paths.contains(song_path)) continue;
|
if (scanned_paths.contains(song_path)) continue;
|
||||||
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
|
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;
|
if (subdir.path != song_path) continue;
|
||||||
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
|
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
|
||||||
const quint64 files_count = FilesCountForPath(&transaction, 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;
|
scanned_paths << subdir.path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Strawberry Music Player
|
* Strawberry Music Player
|
||||||
* This file was part of Clementine.
|
* This file was part of Clementine.
|
||||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
* Copyright 2018-2026, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
*
|
*
|
||||||
* Strawberry is free software: you can redistribute it and/or modify
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -85,6 +85,7 @@ class CollectionWatcher : public QObject {
|
|||||||
void SongsReadded(const SongList &songs, const bool unavailable = false);
|
void SongsReadded(const SongList &songs, const bool unavailable = false);
|
||||||
void SubdirsDiscovered(const CollectionSubdirectoryList &subdirs);
|
void SubdirsDiscovered(const CollectionSubdirectoryList &subdirs);
|
||||||
void SubdirsMTimeUpdated(const CollectionSubdirectoryList &subdirs);
|
void SubdirsMTimeUpdated(const CollectionSubdirectoryList &subdirs);
|
||||||
|
void SubdirsDeleted(const CollectionSubdirectoryList &subdirs);
|
||||||
void CompilationsNeedUpdating();
|
void CompilationsNeedUpdating();
|
||||||
void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
|
void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
|
||||||
void ExitFinished();
|
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.
|
// Emits the signals for new & deleted songs etc and clears the lists. This causes the new stuff to be updated on UI.
|
||||||
void CommitNewOrUpdatedSongs();
|
void CommitNewOrUpdatedSongs();
|
||||||
|
|
||||||
int dir() const { return dir_; }
|
int dir_id() const { return dir_id_; }
|
||||||
bool is_incremental() const { return incremental_; }
|
bool is_incremental() const { return incremental_; }
|
||||||
bool ignores_mtime() const { return ignores_mtime_; }
|
bool ignores_mtime() const { return ignores_mtime_; }
|
||||||
|
|
||||||
@@ -137,13 +138,13 @@ class CollectionWatcher : public QObject {
|
|||||||
QStringList files_changed_path_;
|
QStringList files_changed_path_;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ScanTransaction &operator=(const ScanTransaction&) { return *this; }
|
ScanTransaction &operator=(const ScanTransaction &transaction) { Q_UNUSED(transaction); return *this; }
|
||||||
|
|
||||||
int task_id_;
|
int task_id_;
|
||||||
quint64 progress_;
|
quint64 progress_;
|
||||||
quint64 progress_max_;
|
quint64 progress_max_;
|
||||||
|
|
||||||
int dir_;
|
int dir_id_;
|
||||||
// Incremental scan enters a directory only if it has changed since the last scan.
|
// Incremental scan enters a directory only if it has changed since the last scan.
|
||||||
bool incremental_;
|
bool incremental_;
|
||||||
// This type of scan updates every file in a folder that's being scanned.
|
// 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 IncrementalScanNow();
|
||||||
void FullScanNow();
|
void FullScanNow();
|
||||||
void RescanPathsNow();
|
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);
|
void RescanSongs(const SongList &songs);
|
||||||
|
|
||||||
private:
|
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.
|
// 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;
|
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.
|
// 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.
|
// 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).
|
// 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;
|
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;
|
static QStringList sValidImages;
|
||||||
|
|
||||||
qint64 last_scan_time_;
|
qint64 last_scan_time_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
inline QString CollectionWatcher::NoExtensionPart(const QString &fileName) {
|
inline QString CollectionWatcher::NoExtensionPart(const QString &fileName) {
|
||||||
|
|||||||
@@ -70,6 +70,6 @@ enum class BackgroundImagePosition {
|
|||||||
BottomRight = 5
|
BottomRight = 5
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace AppearanceSettings
|
||||||
|
|
||||||
#endif // APPEARANCESETTINGS_H
|
#endif // APPEARANCESETTINGS_H
|
||||||
|
|||||||
@@ -63,6 +63,6 @@ constexpr qint64 kDefaultBufferDuration = 4000;
|
|||||||
constexpr double kDefaultBufferLowWatermark = 0.33;
|
constexpr double kDefaultBufferLowWatermark = 0.33;
|
||||||
constexpr double kDefaultBufferHighWatermark = 0.99;
|
constexpr double kDefaultBufferHighWatermark = 0.99;
|
||||||
|
|
||||||
} // namespace
|
} // namespace BackendSettings
|
||||||
|
|
||||||
#endif // BACKENDSETTINGS_H
|
#endif // BACKENDSETTINGS_H
|
||||||
|
|||||||
@@ -71,6 +71,6 @@ constexpr char kDoubleClickPlaylistAddMode[] = "doubleclick_playlist_addmode";
|
|||||||
constexpr char kSeekStepSec[] = "seek_step_sec";
|
constexpr char kSeekStepSec[] = "seek_step_sec";
|
||||||
constexpr char kVolumeIncrement[] = "volume_increment";
|
constexpr char kVolumeIncrement[] = "volume_increment";
|
||||||
|
|
||||||
} // namespace
|
} // namespace BehaviourSettings
|
||||||
|
|
||||||
#endif // BEHAVIOURSETTINGS_H
|
#endif // BEHAVIOURSETTINGS_H
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ constexpr char kPrettyCovers[] = "pretty_covers";
|
|||||||
constexpr char kVariousArtists[] = "various_artists";
|
constexpr char kVariousArtists[] = "various_artists";
|
||||||
constexpr char kSkipArticlesForArtists[] = "skip_articles_for_artists";
|
constexpr char kSkipArticlesForArtists[] = "skip_articles_for_artists";
|
||||||
constexpr char kSkipArticlesForAlbums[] = "skip_articles_for_albums";
|
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 kSettingsCacheSize[] = "cache_size";
|
||||||
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
|
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
|
||||||
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";
|
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";
|
||||||
@@ -59,6 +59,6 @@ enum class CacheSizeUnit {
|
|||||||
TB
|
TB
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace CollectionSettings
|
||||||
|
|
||||||
#endif // COLLECTIONSETTINGS_H
|
#endif // COLLECTIONSETTINGS_H
|
||||||
|
|||||||
@@ -43,6 +43,6 @@ constexpr char kSettingsSummaryFmt[] = "SummaryFmt";
|
|||||||
constexpr char kDefaultFontFamily[] = "Noto Sans";
|
constexpr char kDefaultFontFamily[] = "Noto Sans";
|
||||||
constexpr qreal kDefaultFontSizeHeadline = 11;
|
constexpr qreal kDefaultFontSizeHeadline = 11;
|
||||||
|
|
||||||
} // namespace
|
} // namespace ContextSettings
|
||||||
|
|
||||||
#endif // CONTEXTSETTINGS_H
|
#endif // CONTEXTSETTINGS_H
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ constexpr char kSaveOverwrite[] = "save_overwrite";
|
|||||||
constexpr char kSaveLowercase[] = "save_lowercase";
|
constexpr char kSaveLowercase[] = "save_lowercase";
|
||||||
constexpr char kSaveReplaceSpaces[] = "save_replace_spaces";
|
constexpr char kSaveReplaceSpaces[] = "save_replace_spaces";
|
||||||
|
|
||||||
} // namespace
|
} // namespace CoversSettings
|
||||||
|
|
||||||
#endif // COVERSSETTINGS_H
|
#endif // COVERSSETTINGS_H
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ constexpr char kFileFilter[] =
|
|||||||
"*.mod *.s3m *.xm *.it "
|
"*.mod *.s3m *.xm *.it "
|
||||||
"*.spc *.vgm";
|
"*.spc *.vgm";
|
||||||
|
|
||||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
|
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm *.webp)");
|
||||||
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
|
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm *.webp)");
|
||||||
|
|
||||||
#endif // FILEFILTERCONSTANTS_H
|
#endif // FILEFILTERCONSTANTS_H
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ constexpr char kSettingsGroup[] = "GlobalShortcuts";
|
|||||||
constexpr char kUseKGlobalAccel[] = "use_kglobalaccel";
|
constexpr char kUseKGlobalAccel[] = "use_kglobalaccel";
|
||||||
constexpr char kUseX11[] = "use_x11";
|
constexpr char kUseX11[] = "use_x11";
|
||||||
|
|
||||||
} // namespace
|
} // namespace GlobalShortcutsSettings
|
||||||
|
|
||||||
#endif // GLOBALSHORTCUTSSETTINGS_H
|
#endif // GLOBALSHORTCUTSSETTINGS_H
|
||||||
|
|||||||
@@ -25,6 +25,6 @@ namespace LyricsSettings {
|
|||||||
constexpr char kSettingsGroup[] = "Lyrics";
|
constexpr char kSettingsGroup[] = "Lyrics";
|
||||||
constexpr char kProviders[] = "providers";
|
constexpr char kProviders[] = "providers";
|
||||||
|
|
||||||
} // namespace
|
} // namespace LyricsSettings
|
||||||
|
|
||||||
#endif // LYRICSSETTINGS_H
|
#endif // LYRICSSETTINGS_H
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ constexpr char kGeometry[] = "geometry";
|
|||||||
constexpr char kSplitterState[] = "splitter_state";
|
constexpr char kSplitterState[] = "splitter_state";
|
||||||
constexpr char kDoNotShowSponsorMessage[] = "do_not_show_sponsor_message";
|
constexpr char kDoNotShowSponsorMessage[] = "do_not_show_sponsor_message";
|
||||||
|
|
||||||
} // namespace
|
} // namespace MainWindowSettings
|
||||||
|
|
||||||
#endif // MAINWINDOWSETTINGS_H
|
#endif // MAINWINDOWSETTINGS_H
|
||||||
|
|||||||
@@ -38,6 +38,6 @@ constexpr char kShow[] = "show";
|
|||||||
constexpr char kStyle[] = "style";
|
constexpr char kStyle[] = "style";
|
||||||
constexpr char kSave[] = "save";
|
constexpr char kSave[] = "save";
|
||||||
|
|
||||||
} // namespace
|
} // namespace MoodbarSettings
|
||||||
|
|
||||||
#endif // MOODBARSETTINGS_H
|
#endif // MOODBARSETTINGS_H
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ constexpr char kUsername[] = "username";
|
|||||||
constexpr char kPassword[] = "password";
|
constexpr char kPassword[] = "password";
|
||||||
constexpr char kEngine[] = "engine";
|
constexpr char kEngine[] = "engine";
|
||||||
|
|
||||||
} // namespace
|
} // namespace NetworkProxySettings
|
||||||
|
|
||||||
#endif // NETWORKPROXYSETTINGS_H
|
#endif // NETWORKPROXYSETTINGS_H
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ constexpr char kCustomTextEnabled[] = "CustomTextEnabled";
|
|||||||
constexpr char kCustomText1[] = "CustomText1";
|
constexpr char kCustomText1[] = "CustomText1";
|
||||||
constexpr char kCustomText2[] = "CustomText2";
|
constexpr char kCustomText2[] = "CustomText2";
|
||||||
|
|
||||||
} // namespace
|
} // namespace OSDSettings
|
||||||
|
|
||||||
namespace OSDPrettySettings {
|
namespace OSDPrettySettings {
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ constexpr char kFading[] = "fading";
|
|||||||
constexpr QRgb kPresetBlue = qRgb(102, 150, 227);
|
constexpr QRgb kPresetBlue = qRgb(102, 150, 227);
|
||||||
constexpr QRgb kPresetRed = qRgb(202, 22, 16);
|
constexpr QRgb kPresetRed = qRgb(202, 22, 16);
|
||||||
|
|
||||||
} // namespace
|
} // namespace OSDPrettySettings
|
||||||
|
|
||||||
namespace DiscordRPCSettings {
|
namespace DiscordRPCSettings {
|
||||||
|
|
||||||
@@ -79,6 +79,6 @@ enum class StatusDisplayType {
|
|||||||
Song
|
Song
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace DiscordRPCSettings
|
||||||
|
|
||||||
#endif // NOTIFICATIONSSETTINGS_H
|
#endif // NOTIFICATIONSSETTINGS_H
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ constexpr char kLastSaveExtension[] = "last_save_extension";
|
|||||||
constexpr char kLastSaveAllPath[] = "last_save_all_path";
|
constexpr char kLastSaveAllPath[] = "last_save_all_path";
|
||||||
constexpr char kLastSaveAllExtension[] = "last_save_all_extension";
|
constexpr char kLastSaveAllExtension[] = "last_save_all_extension";
|
||||||
|
|
||||||
} // namespace
|
} // namespace PlaylistSettings
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(PlaylistSettings::PathType)
|
Q_DECLARE_METATYPE(PlaylistSettings::PathType)
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,6 @@ constexpr char kCredentialsId[] = "credentials_id";
|
|||||||
constexpr char kDeviceId[] = "device_id";
|
constexpr char kDeviceId[] = "device_id";
|
||||||
constexpr char kUserAuthToken[] = "user_auth_token";
|
constexpr char kUserAuthToken[] = "user_auth_token";
|
||||||
|
|
||||||
} // namespace
|
} // namespace QobuzSettings
|
||||||
|
|
||||||
#endif // QOBUZSETTINGS_H
|
#endif // QOBUZSETTINGS_H
|
||||||
|
|||||||
@@ -35,6 +35,6 @@ constexpr char kStripRemastered[] = "strip_remastered";
|
|||||||
constexpr char kSources[] = "sources";
|
constexpr char kSources[] = "sources";
|
||||||
constexpr char kUserToken[] = "user_token";
|
constexpr char kUserToken[] = "user_token";
|
||||||
|
|
||||||
} // namespace
|
} // namespace ScrobblerSettings
|
||||||
|
|
||||||
#endif // SCROBBLERSETTINGS_H
|
#endif // SCROBBLERSETTINGS_H
|
||||||
|
|||||||
@@ -38,6 +38,6 @@ constexpr char kRefreshToken[] = "refresh_token";
|
|||||||
constexpr char kExpiresIn[] = "expires_in";
|
constexpr char kExpiresIn[] = "expires_in";
|
||||||
constexpr char kLoginTime[] = "login_time";
|
constexpr char kLoginTime[] = "login_time";
|
||||||
|
|
||||||
} // namespace
|
} // namespace SpotifySettings
|
||||||
|
|
||||||
#endif // SPOTIFYSETTINGS_H
|
#endif // SPOTIFYSETTINGS_H
|
||||||
|
|||||||
@@ -41,6 +41,6 @@ constexpr char kUseAlbumIdForAlbumCovers[] = "usealbumidforalbumcovers";
|
|||||||
constexpr char kServerSideScrobbling[] = "serversidescrobbling";
|
constexpr char kServerSideScrobbling[] = "serversidescrobbling";
|
||||||
constexpr char kAuthMethod[] = "authmethod";
|
constexpr char kAuthMethod[] = "authmethod";
|
||||||
|
|
||||||
} // namespace
|
} // namespace SubsonicSettings
|
||||||
|
|
||||||
#endif // SUBSONICETTINGS_H
|
#endif // SUBSONICETTINGS_H
|
||||||
|
|||||||
@@ -48,6 +48,6 @@ enum class StreamUrlMethod {
|
|||||||
PlaybackInfoPostPaywall
|
PlaybackInfoPostPaywall
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
} // namespace TidalSettings
|
||||||
|
|
||||||
#endif // TIDALSETTINGS_H
|
#endif // TIDALSETTINGS_H
|
||||||
|
|||||||
@@ -26,4 +26,3 @@ constexpr char kSettingsGroup[] = "Transcoder";
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endif // TRANSCODERSETTINGS_H
|
#endif // TRANSCODERSETTINGS_H
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ class ContextAlbum : public QWidget {
|
|||||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
struct PreviousCover {
|
struct PreviousCover {
|
||||||
explicit PreviousCover() : opacity(0.0) {}
|
explicit PreviousCover() : opacity(0.0) {}
|
||||||
QImage image;
|
QImage image;
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
#include "covermanager/albumcoverchoicecontroller.h"
|
#include "covermanager/albumcoverchoicecontroller.h"
|
||||||
#include "lyrics/lyricsfetcher.h"
|
#include "lyrics/lyricsfetcher.h"
|
||||||
#include "constants/contextsettings.h"
|
#include "constants/contextsettings.h"
|
||||||
|
#include "constants/timeconstants.h"
|
||||||
|
|
||||||
#include "contextview.h"
|
#include "contextview.h"
|
||||||
#include "contextalbum.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) {
|
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_fetcher_->Clear();
|
||||||
lyrics_tried_ = true;
|
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:
|
protected:
|
||||||
void resizeEvent(QResizeEvent *e) override;
|
void resizeEvent(QResizeEvent *e) override;
|
||||||
void contextMenuEvent(QContextMenuEvent*) override;
|
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||||
void dragEnterEvent(QDragEnterEvent*) override;
|
void dragEnterEvent(QDragEnterEvent *e) override;
|
||||||
void dropEvent(QDropEvent*) override;
|
void dropEvent(QDropEvent *e) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void AddActions();
|
void AddActions();
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
#include "lyrics/elyricsnetlyricsprovider.h"
|
#include "lyrics/elyricsnetlyricsprovider.h"
|
||||||
#include "lyrics/letraslyricsprovider.h"
|
#include "lyrics/letraslyricsprovider.h"
|
||||||
#include "lyrics/lyricfindlyricsprovider.h"
|
#include "lyrics/lyricfindlyricsprovider.h"
|
||||||
|
#include "lyrics/lrcliblyricsprovider.h"
|
||||||
|
|
||||||
#include "scrobbler/audioscrobbler.h"
|
#include "scrobbler/audioscrobbler.h"
|
||||||
#include "scrobbler/lastfmscrobbler.h"
|
#include "scrobbler/lastfmscrobbler.h"
|
||||||
@@ -117,8 +118,8 @@ using namespace std::chrono_literals;
|
|||||||
|
|
||||||
class ApplicationImpl {
|
class ApplicationImpl {
|
||||||
public:
|
public:
|
||||||
explicit ApplicationImpl(Application *app) :
|
explicit ApplicationImpl(Application *app)
|
||||||
tagreader_client_([app](){
|
: tagreader_client_([app]() {
|
||||||
TagReaderClient *client = new TagReaderClient();
|
TagReaderClient *client = new TagReaderClient();
|
||||||
app->MoveToNewThread(client);
|
app->MoveToNewThread(client);
|
||||||
return client;
|
return client;
|
||||||
@@ -182,6 +183,7 @@ class ApplicationImpl {
|
|||||||
lyrics_providers->AddProvider(new ElyricsNetLyricsProvider(lyrics_providers->network()));
|
lyrics_providers->AddProvider(new ElyricsNetLyricsProvider(lyrics_providers->network()));
|
||||||
lyrics_providers->AddProvider(new LetrasLyricsProvider(lyrics_providers->network()));
|
lyrics_providers->AddProvider(new LetrasLyricsProvider(lyrics_providers->network()));
|
||||||
lyrics_providers->AddProvider(new LyricFindLyricsProvider(lyrics_providers->network()));
|
lyrics_providers->AddProvider(new LyricFindLyricsProvider(lyrics_providers->network()));
|
||||||
|
lyrics_providers->AddProvider(new LrcLibLyricsProvider(lyrics_providers->network()));
|
||||||
lyrics_providers->ReloadSettings();
|
lyrics_providers->ReloadSettings();
|
||||||
return lyrics_providers;
|
return lyrics_providers;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ constexpr char kMagicAllSongsTables[] = "%allsongstables";
|
|||||||
int Database::sNextConnectionId = 1;
|
int Database::sNextConnectionId = 1;
|
||||||
QMutex Database::sNextConnectionIdMutex;
|
QMutex Database::sNextConnectionIdMutex;
|
||||||
|
|
||||||
Database::Database(SharedPtr<TaskManager> task_manager, QObject *parent, const QString &database_name) :
|
Database::Database(SharedPtr<TaskManager> task_manager, QObject *parent, const QString &database_name)
|
||||||
QObject(parent),
|
: QObject(parent),
|
||||||
task_manager_(task_manager),
|
task_manager_(task_manager),
|
||||||
injected_database_name_(database_name),
|
injected_database_name_(database_name),
|
||||||
query_hash_(0),
|
query_hash_(0),
|
||||||
@@ -503,7 +503,9 @@ bool Database::IntegrityCheck(const QSqlDatabase &db) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!error_reported) { Q_EMIT Error(tr("Database corruption detected.")); }
|
if (!error_reported) {
|
||||||
|
Q_EMIT Error(tr("Database corruption detected."));
|
||||||
|
}
|
||||||
Q_EMIT Error(u"Database: "_s + message);
|
Q_EMIT Error(u"Database: "_s + message);
|
||||||
error_reported = true;
|
error_reported = true;
|
||||||
}
|
}
|
||||||
@@ -594,8 +596,7 @@ void Database::BackupFile(const QString &filename) {
|
|||||||
ret = sqlite3_backup_step(backup, 16);
|
ret = sqlite3_backup_step(backup, 16);
|
||||||
const int page_count = sqlite3_backup_pagecount(backup);
|
const int page_count = sqlite3_backup_pagecount(backup);
|
||||||
task_manager_->SetTaskProgress(task_id, static_cast<quint64>(page_count - sqlite3_backup_remaining(backup)), static_cast<quint64>(page_count));
|
task_manager_->SetTaskProgress(task_id, static_cast<quint64>(page_count - sqlite3_backup_remaining(backup)), static_cast<quint64>(page_count));
|
||||||
}
|
} while (ret == SQLITE_OK);
|
||||||
while (ret == SQLITE_OK);
|
|
||||||
|
|
||||||
if (ret != SQLITE_DONE) {
|
if (ret != SQLITE_DONE) {
|
||||||
qLog(Error) << "Database backup failed";
|
qLog(Error) << "Database backup failed";
|
||||||
|
|||||||
@@ -128,7 +128,6 @@ class Database : public QObject {
|
|||||||
int startup_schema_version_;
|
int startup_schema_version_;
|
||||||
|
|
||||||
QThread *original_thread_;
|
QThread *original_thread_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // DATABASE_H
|
#endif // DATABASE_H
|
||||||
|
|||||||
@@ -110,21 +110,32 @@ bool FilesystemMusicStorage::CopyToStorage(const CopyJob &job, QString &error_te
|
|||||||
|
|
||||||
bool FilesystemMusicStorage::DeleteFromStorage(const DeleteJob &job) {
|
bool FilesystemMusicStorage::DeleteFromStorage(const DeleteJob &job) {
|
||||||
|
|
||||||
QString path = job.metadata_.url().toLocalFile();
|
const QString path = job.metadata_.url().toLocalFile();
|
||||||
QFileInfo fileInfo(path);
|
const QFileInfo fileInfo(path);
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
|
||||||
if (job.use_trash_ && QFile::supportsMoveToTrash()) {
|
if (job.use_trash_ && QFile::supportsMoveToTrash()) {
|
||||||
#else
|
#else
|
||||||
if (job.use_trash_) {
|
if (job.use_trash_) {
|
||||||
#endif
|
#endif
|
||||||
return QFile::moveToTrash(path);
|
if (QFile::moveToTrash(path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
qLog(Warning) << "Moving file to trash failed for" << path << ", falling back to direct deletion";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
if (fileInfo.isDir()) {
|
if (fileInfo.isDir()) {
|
||||||
return Utilities::RemoveRecursive(path);
|
success = Utilities::RemoveRecursive(path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
success = QFile::remove(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return QFile::remove(path);
|
if (!success) {
|
||||||
|
qLog(Error) << "Failed to delete file" << path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class IconLoader {
|
|||||||
public:
|
public:
|
||||||
static void Init();
|
static void Init();
|
||||||
static QIcon Load(const QString &name, const bool system_icon = true, const int fixed_size = 0, const int min_size = 0, const int max_size = 0);
|
static QIcon Load(const QString &name, const bool system_icon = true, const int fixed_size = 0, const int min_size = 0, const int max_size = 0);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit IconLoader() {}
|
explicit IconLoader() {}
|
||||||
static bool system_icons_;
|
static bool system_icons_;
|
||||||
|
|||||||
@@ -155,11 +155,7 @@ void LocalRedirectServer::WriteTemplate() const {
|
|||||||
|
|
||||||
QBuffer image_buffer;
|
QBuffer image_buffer;
|
||||||
if (image_buffer.open(QIODevice::ReadWrite)) {
|
if (image_buffer.open(QIODevice::ReadWrite)) {
|
||||||
QApplication::style()
|
QApplication::style()->standardIcon(QStyle::SP_DialogOkButton).pixmap(16).toImage().save(&image_buffer, "PNG");
|
||||||
->standardIcon(QStyle::SP_DialogOkButton)
|
|
||||||
.pixmap(16)
|
|
||||||
.toImage()
|
|
||||||
.save(&image_buffer, "PNG");
|
|
||||||
page_data.replace("@IMAGE_DATA@"_L1, QString::fromUtf8(image_buffer.data().toBase64()));
|
page_data.replace("@IMAGE_DATA@"_L1, QString::fromUtf8(image_buffer.data().toBase64()));
|
||||||
image_buffer.close();
|
image_buffer.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@
|
|||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QErrorMessage>
|
#include <QErrorMessage>
|
||||||
#include <QSettings>
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QItemSelectionModel>
|
#include <QItemSelectionModel>
|
||||||
@@ -174,9 +173,12 @@
|
|||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_SPOTIFY
|
#ifdef HAVE_SPOTIFY
|
||||||
# include "spotify/spotifyservice.h"
|
# include "spotify/spotifyservice.h"
|
||||||
|
# include "spotify/spotifymetadatarequest.h"
|
||||||
# include "constants/spotifysettings.h"
|
# include "constants/spotifysettings.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_QOBUZ
|
#ifdef HAVE_QOBUZ
|
||||||
|
# include "qobuz/qobuzservice.h"
|
||||||
|
# include "qobuz/qobuzmetadatarequest.h"
|
||||||
# include "constants/qobuzsettings.h"
|
# include "constants/qobuzsettings.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -296,9 +298,6 @@ MainWindow::MainWindow(Application *app,
|
|||||||
#ifdef HAVE_DISCORD_RPC
|
#ifdef HAVE_DISCORD_RPC
|
||||||
discord_rich_presence_(discord_rich_presence),
|
discord_rich_presence_(discord_rich_presence),
|
||||||
#endif
|
#endif
|
||||||
error_dialog_([this]() {
|
|
||||||
return new ErrorDialog(this);
|
|
||||||
}),
|
|
||||||
console_([app, this]() {
|
console_([app, this]() {
|
||||||
Console *console = new Console(app->database());
|
Console *console = new Console(app->database());
|
||||||
QObject::connect(console, &Console::Error, this, &MainWindow::ShowErrorDialog);
|
QObject::connect(console, &Console::Error, this, &MainWindow::ShowErrorDialog);
|
||||||
@@ -383,8 +382,10 @@ MainWindow::MainWindow(Application *app,
|
|||||||
playlist_add_to_another_(nullptr),
|
playlist_add_to_another_(nullptr),
|
||||||
playlistitem_actions_separator_(nullptr),
|
playlistitem_actions_separator_(nullptr),
|
||||||
playlist_rescan_songs_(nullptr),
|
playlist_rescan_songs_(nullptr),
|
||||||
|
playlist_fetch_metadata_(nullptr),
|
||||||
track_position_timer_(new QTimer(this)),
|
track_position_timer_(new QTimer(this)),
|
||||||
track_slider_timer_(new QTimer(this)),
|
track_slider_timer_(new QTimer(this)),
|
||||||
|
metadata_queue_timer_(new QTimer(this)),
|
||||||
keep_running_(false),
|
keep_running_(false),
|
||||||
playing_widget_(true),
|
playing_widget_(true),
|
||||||
#ifdef HAVE_DBUS
|
#ifdef HAVE_DBUS
|
||||||
@@ -456,6 +457,10 @@ MainWindow::MainWindow(Application *app,
|
|||||||
track_slider_timer_->setInterval(kTrackSliderUpdateTimeMs);
|
track_slider_timer_->setInterval(kTrackSliderUpdateTimeMs);
|
||||||
QObject::connect(track_slider_timer_, &QTimer::timeout, this, &MainWindow::UpdateTrackSliderPosition);
|
QObject::connect(track_slider_timer_, &QTimer::timeout, this, &MainWindow::UpdateTrackSliderPosition);
|
||||||
|
|
||||||
|
metadata_queue_timer_->setInterval(200ms); // 200ms between requests to avoid rate limiting
|
||||||
|
metadata_queue_timer_->setSingleShot(true);
|
||||||
|
QObject::connect(metadata_queue_timer_, &QTimer::timeout, this, &MainWindow::ProcessMetadataQueue);
|
||||||
|
|
||||||
// Start initializing the player
|
// Start initializing the player
|
||||||
qLog(Debug) << "Initializing player";
|
qLog(Debug) << "Initializing player";
|
||||||
app_->player()->SetAnalyzer(ui_->analyzer);
|
app_->player()->SetAnalyzer(ui_->analyzer);
|
||||||
@@ -700,6 +705,9 @@ MainWindow::MainWindow(Application *app,
|
|||||||
QObject::connect(&*app_->task_manager(), &TaskManager::PauseCollectionWatchers, &*app_->collection(), &CollectionLibrary::PauseWatcher);
|
QObject::connect(&*app_->task_manager(), &TaskManager::PauseCollectionWatchers, &*app_->collection(), &CollectionLibrary::PauseWatcher);
|
||||||
QObject::connect(&*app_->task_manager(), &TaskManager::ResumeCollectionWatchers, &*app_->collection(), &CollectionLibrary::ResumeWatcher);
|
QObject::connect(&*app_->task_manager(), &TaskManager::ResumeCollectionWatchers, &*app_->collection(), &CollectionLibrary::ResumeWatcher);
|
||||||
|
|
||||||
|
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, &*app_->collection(), &CollectionLibrary::CurrentSongChanged);
|
||||||
|
QObject::connect(&*app_->player(), &Player::Stopped, &*app_->collection(), &CollectionLibrary::Stopped);
|
||||||
|
|
||||||
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, &*app_->current_albumcover_loader(), &CurrentAlbumCoverLoader::LoadAlbumCover);
|
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, &*app_->current_albumcover_loader(), &CurrentAlbumCoverLoader::LoadAlbumCover);
|
||||||
QObject::connect(&*app_->current_albumcover_loader(), &CurrentAlbumCoverLoader::AlbumCoverLoaded, this, &MainWindow::AlbumCoverLoaded);
|
QObject::connect(&*app_->current_albumcover_loader(), &CurrentAlbumCoverLoader::AlbumCoverLoaded, this, &MainWindow::AlbumCoverLoaded);
|
||||||
QObject::connect(album_cover_choice_controller_, &AlbumCoverChoiceController::Error, this, &MainWindow::ShowErrorDialog);
|
QObject::connect(album_cover_choice_controller_, &AlbumCoverChoiceController::Error, this, &MainWindow::ShowErrorDialog);
|
||||||
@@ -813,6 +821,8 @@ MainWindow::MainWindow(Application *app,
|
|||||||
#endif
|
#endif
|
||||||
playlist_rescan_songs_ = playlist_menu_->addAction(IconLoader::Load(u"view-refresh"_s), tr("Rescan song(s)..."), this, &MainWindow::RescanSongs);
|
playlist_rescan_songs_ = playlist_menu_->addAction(IconLoader::Load(u"view-refresh"_s), tr("Rescan song(s)..."), this, &MainWindow::RescanSongs);
|
||||||
playlist_menu_->addAction(playlist_rescan_songs_);
|
playlist_menu_->addAction(playlist_rescan_songs_);
|
||||||
|
playlist_fetch_metadata_ = playlist_menu_->addAction(IconLoader::Load(u"download"_s), tr("Fetch metadata from service"), this, &MainWindow::FetchStreamingMetadata);
|
||||||
|
playlist_menu_->addAction(playlist_fetch_metadata_);
|
||||||
playlist_menu_->addAction(ui_->action_add_files_to_transcoder);
|
playlist_menu_->addAction(ui_->action_add_files_to_transcoder);
|
||||||
playlist_menu_->addSeparator();
|
playlist_menu_->addSeparator();
|
||||||
playlist_copy_url_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy URL(s)..."), this, &MainWindow::PlaylistCopyUrl);
|
playlist_copy_url_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy URL(s)..."), this, &MainWindow::PlaylistCopyUrl);
|
||||||
@@ -980,27 +990,28 @@ MainWindow::MainWindow(Application *app,
|
|||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
qLog(Debug) << "Loading settings";
|
qLog(Debug) << "Loading settings";
|
||||||
settings_.beginGroup(MainWindowSettings::kSettingsGroup);
|
Settings settings;
|
||||||
|
settings.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
|
|
||||||
// Set last used geometry to position window on the correct monitor
|
// Set last used geometry to position window on the correct monitor
|
||||||
// Set window state only if the window was last maximized
|
// Set window state only if the window was last maximized
|
||||||
if (settings_.contains("geometry")) {
|
if (settings.contains("geometry")) {
|
||||||
restoreGeometry(settings_.value("geometry").toByteArray());
|
restoreGeometry(settings.value("geometry").toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!settings_.contains(MainWindowSettings::kSplitterState) || !ui_->splitter->restoreState(settings_.value(MainWindowSettings::kSplitterState).toByteArray())) {
|
if (!settings.contains(MainWindowSettings::kSplitterState) || !ui_->splitter->restoreState(settings.value(MainWindowSettings::kSplitterState).toByteArray())) {
|
||||||
ui_->splitter->setSizes(QList<int>() << 20 << (width() - 20));
|
ui_->splitter->setSizes(QList<int>() << 20 << (width() - 20));
|
||||||
}
|
}
|
||||||
|
|
||||||
ui_->tabs->setCurrentIndex(settings_.value("current_tab", 1).toInt());
|
ui_->tabs->setCurrentIndex(settings.value("current_tab", 1).toInt());
|
||||||
FancyTabWidget::Mode default_mode = FancyTabWidget::Mode::LargeSidebar;
|
FancyTabWidget::Mode default_mode = FancyTabWidget::Mode::LargeSidebar;
|
||||||
FancyTabWidget::Mode tab_mode = static_cast<FancyTabWidget::Mode>(settings_.value("tab_mode", static_cast<int>(default_mode)).toInt());
|
FancyTabWidget::Mode tab_mode = static_cast<FancyTabWidget::Mode>(settings.value("tab_mode", static_cast<int>(default_mode)).toInt());
|
||||||
if (tab_mode == FancyTabWidget::Mode::None) tab_mode = default_mode;
|
if (tab_mode == FancyTabWidget::Mode::None) tab_mode = default_mode;
|
||||||
ui_->tabs->SetMode(tab_mode);
|
ui_->tabs->SetMode(tab_mode);
|
||||||
|
|
||||||
TabSwitched();
|
TabSwitched();
|
||||||
|
|
||||||
file_view_->SetPath(settings_.value("file_path", QDir::homePath()).toString());
|
file_view_->SetPath(settings.value("file_path", QDir::homePath()).toString());
|
||||||
|
|
||||||
// Users often collapse one side of the splitter by mistake and don't know how to restore it. This must be set after the state is restored above.
|
// Users often collapse one side of the splitter by mistake and don't know how to restore it. This must be set after the state is restored above.
|
||||||
ui_->splitter->setChildrenCollapsible(false);
|
ui_->splitter->setChildrenCollapsible(false);
|
||||||
@@ -1043,13 +1054,13 @@ MainWindow::MainWindow(Application *app,
|
|||||||
case BehaviourSettings::StartupBehaviour::Remember:
|
case BehaviourSettings::StartupBehaviour::Remember:
|
||||||
default:{
|
default:{
|
||||||
|
|
||||||
was_maximized_ = settings_.value(MainWindowSettings::kMaximized, true).toBool();
|
was_maximized_ = settings.value(MainWindowSettings::kMaximized, true).toBool();
|
||||||
if (was_maximized_) setWindowState(windowState() | Qt::WindowMaximized);
|
if (was_maximized_) setWindowState(windowState() | Qt::WindowMaximized);
|
||||||
|
|
||||||
was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool();
|
was_minimized_ = settings.value(MainWindowSettings::kMinimized, false).toBool();
|
||||||
if (was_minimized_) setWindowState(windowState() | Qt::WindowMinimized);
|
if (was_minimized_) setWindowState(windowState() | Qt::WindowMinimized);
|
||||||
|
|
||||||
if (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
|
if (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !settings.value(MainWindowSettings::kHidden, false).toBool()) {
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1057,7 +1068,7 @@ MainWindow::MainWindow(Application *app,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool show_sidebar = settings_.value(MainWindowSettings::kShowSidebar, true).toBool();
|
bool show_sidebar = settings.value(MainWindowSettings::kShowSidebar, true).toBool();
|
||||||
ui_->sidebar_layout->setVisible(show_sidebar);
|
ui_->sidebar_layout->setVisible(show_sidebar);
|
||||||
ui_->action_toggle_show_sidebar->setChecked(show_sidebar);
|
ui_->action_toggle_show_sidebar->setChecked(show_sidebar);
|
||||||
|
|
||||||
@@ -1228,7 +1239,9 @@ void MainWindow::ReloadSettings() {
|
|||||||
|
|
||||||
osd_->ReloadSettings();
|
osd_->ReloadSettings();
|
||||||
|
|
||||||
album_cover_choice_controller_->search_cover_auto_action()->setChecked(settings_.value(MainWindowSettings::kSearchForCoverAuto, true).toBool());
|
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
|
album_cover_choice_controller_->search_cover_auto_action()->setChecked(s.value(MainWindowSettings::kSearchForCoverAuto, true).toBool());
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
#ifdef HAVE_SUBSONIC
|
#ifdef HAVE_SUBSONIC
|
||||||
s.beginGroup(SubsonicSettings::kSettingsGroup);
|
s.beginGroup(SubsonicSettings::kSettingsGroup);
|
||||||
@@ -1345,8 +1358,11 @@ void MainWindow::SaveSettings() {
|
|||||||
ui_->playlist->view()->SaveSettings();
|
ui_->playlist->view()->SaveSettings();
|
||||||
app_->scrobbler()->WriteCache();
|
app_->scrobbler()->WriteCache();
|
||||||
|
|
||||||
settings_.setValue(MainWindowSettings::kShowSidebar, ui_->action_toggle_show_sidebar->isChecked());
|
Settings s;
|
||||||
settings_.setValue(MainWindowSettings::kSearchForCoverAuto, album_cover_choice_controller_->search_cover_auto_action()->isChecked());
|
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
|
s.setValue(MainWindowSettings::kShowSidebar, ui_->action_toggle_show_sidebar->isChecked());
|
||||||
|
s.setValue(MainWindowSettings::kSearchForCoverAuto, album_cover_choice_controller_->search_cover_auto_action()->isChecked());
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1587,23 +1603,35 @@ void MainWindow::ToggleSidebar(const bool checked) {
|
|||||||
|
|
||||||
ui_->sidebar_layout->setVisible(checked);
|
ui_->sidebar_layout->setVisible(checked);
|
||||||
TabSwitched();
|
TabSwitched();
|
||||||
settings_.setValue(MainWindowSettings::kShowSidebar, checked);
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
|
s.setValue(MainWindowSettings::kShowSidebar, checked);
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::ToggleSearchCoverAuto(const bool checked) {
|
void MainWindow::ToggleSearchCoverAuto(const bool checked) {
|
||||||
settings_.setValue(MainWindowSettings::kSearchForCoverAuto, checked);
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
|
s.setValue(MainWindowSettings::kSearchForCoverAuto, checked);
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::SaveGeometry() {
|
void MainWindow::SaveGeometry() {
|
||||||
|
|
||||||
if (!initialized_) return;
|
if (!initialized_) return;
|
||||||
|
|
||||||
settings_.setValue(MainWindowSettings::kMaximized, isMaximized());
|
Settings s;
|
||||||
settings_.setValue(MainWindowSettings::kMinimized, isMinimized());
|
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
settings_.setValue(MainWindowSettings::kHidden, isHidden());
|
s.setValue(MainWindowSettings::kMaximized, isMaximized());
|
||||||
settings_.setValue(MainWindowSettings::kGeometry, saveGeometry());
|
s.setValue(MainWindowSettings::kMinimized, isMinimized());
|
||||||
settings_.setValue(MainWindowSettings::kSplitterState, ui_->splitter->saveState());
|
s.setValue(MainWindowSettings::kHidden, isHidden());
|
||||||
|
s.setValue(MainWindowSettings::kGeometry, saveGeometry());
|
||||||
|
s.setValue(MainWindowSettings::kSplitterState, ui_->splitter->saveState());
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1745,7 +1773,12 @@ void MainWindow::SetHiddenInTray(const bool hidden) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::FilePathChanged(const QString &path) {
|
void MainWindow::FilePathChanged(const QString &path) {
|
||||||
settings_.setValue("file_path", path);
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
|
s.setValue("file_path", path);
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::Seeked(const qint64 microseconds) {
|
void MainWindow::Seeked(const qint64 microseconds) {
|
||||||
@@ -1973,6 +2006,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
|||||||
int in_skipped = 0;
|
int in_skipped = 0;
|
||||||
int not_in_skipped = 0;
|
int not_in_skipped = 0;
|
||||||
int local_songs = 0;
|
int local_songs = 0;
|
||||||
|
int streaming_songs = 0;
|
||||||
|
|
||||||
for (const QModelIndex &idx : selection) {
|
for (const QModelIndex &idx : selection) {
|
||||||
|
|
||||||
@@ -1982,7 +2016,13 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
|||||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(src_idx.row());
|
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(src_idx.row());
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
|
|
||||||
if (item->EffectiveMetadata().url().isLocalFile()) ++local_songs;
|
if (item->EffectiveMetadata().url().isLocalFile()) {
|
||||||
|
++local_songs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item->EffectiveMetadata().is_stream_service()) {
|
||||||
|
++streaming_songs;
|
||||||
|
}
|
||||||
|
|
||||||
if (item->EffectiveMetadata().has_cue()) {
|
if (item->EffectiveMetadata().has_cue()) {
|
||||||
cue_selected = true;
|
cue_selected = true;
|
||||||
@@ -2010,6 +2050,9 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
|
|||||||
playlist_rescan_songs_->setEnabled(local_songs > 0 && editable > 0);
|
playlist_rescan_songs_->setEnabled(local_songs > 0 && editable > 0);
|
||||||
playlist_rescan_songs_->setVisible(local_songs > 0 && editable > 0);
|
playlist_rescan_songs_->setVisible(local_songs > 0 && editable > 0);
|
||||||
|
|
||||||
|
playlist_fetch_metadata_->setEnabled(streaming_songs > 0);
|
||||||
|
playlist_fetch_metadata_->setVisible(streaming_songs > 0);
|
||||||
|
|
||||||
ui_->action_add_files_to_transcoder->setEnabled(local_songs > 0 && editable > 0);
|
ui_->action_add_files_to_transcoder->setEnabled(local_songs > 0 && editable > 0);
|
||||||
ui_->action_add_files_to_transcoder->setVisible(local_songs > 0 && editable > 0);
|
ui_->action_add_files_to_transcoder->setVisible(local_songs > 0 && editable > 0);
|
||||||
|
|
||||||
@@ -2221,9 +2264,23 @@ void MainWindow::EditTracks() {
|
|||||||
void MainWindow::EditTagDialogAccepted() {
|
void MainWindow::EditTagDialogAccepted() {
|
||||||
|
|
||||||
const PlaylistItemPtrList items = edit_tag_dialog_->playlist_items();
|
const PlaylistItemPtrList items = edit_tag_dialog_->playlist_items();
|
||||||
for (PlaylistItemPtr item : items) {
|
const SongList songs = edit_tag_dialog_->songs();
|
||||||
|
|
||||||
|
if (items.count() != songs.count()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < items.count(); ++i) {
|
||||||
|
PlaylistItemPtr item = items[i];
|
||||||
|
const Song &updated_song = songs[i];
|
||||||
|
// For stream tracks, apply the metadata directly since there's no file to reload from
|
||||||
|
if (updated_song.is_stream_service()) {
|
||||||
|
item->SetOriginalMetadata(updated_song);
|
||||||
|
}
|
||||||
|
else {
|
||||||
item->Reload();
|
item->Reload();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: This is really lame but we don't know what rows have changed.
|
// FIXME: This is really lame but we don't know what rows have changed.
|
||||||
ui_->playlist->view()->update();
|
ui_->playlist->view()->update();
|
||||||
@@ -2297,8 +2354,8 @@ void MainWindow::SelectionSetValue() {
|
|||||||
QObject::disconnect(*connection);
|
QObject::disconnect(*connection);
|
||||||
}, Qt::QueuedConnection);
|
}, Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
else if (song.source() == Song::Source::Stream) {
|
else if (song.is_stream()) {
|
||||||
app_->playlist_manager()->current()->setData(source_index, column_value, 0);
|
app_->playlist_manager()->current()->setData(source_index.sibling(source_index.row(), static_cast<int>(column)), column_value, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2327,7 +2384,9 @@ void MainWindow::EditValue() {
|
|||||||
void MainWindow::AddFile() {
|
void MainWindow::AddFile() {
|
||||||
|
|
||||||
// Last used directory
|
// Last used directory
|
||||||
QString directory = settings_.value("add_media_path", QDir::currentPath()).toString();
|
Settings s;
|
||||||
|
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
|
QString directory = s.value("add_media_path", QDir::currentPath()).toString();
|
||||||
|
|
||||||
PlaylistParser parser(app_->tagreader_client(), app_->collection_backend());
|
PlaylistParser parser(app_->tagreader_client(), app_->collection_backend());
|
||||||
|
|
||||||
@@ -2337,7 +2396,7 @@ void MainWindow::AddFile() {
|
|||||||
if (filenames.isEmpty()) return;
|
if (filenames.isEmpty()) return;
|
||||||
|
|
||||||
// Save last used directory
|
// Save last used directory
|
||||||
settings_.setValue("add_media_path", filenames[0]);
|
s.setValue("add_media_path", filenames[0]);
|
||||||
|
|
||||||
// Convert to URLs
|
// Convert to URLs
|
||||||
QList<QUrl> urls;
|
QList<QUrl> urls;
|
||||||
@@ -2355,14 +2414,16 @@ void MainWindow::AddFile() {
|
|||||||
void MainWindow::AddFolder() {
|
void MainWindow::AddFolder() {
|
||||||
|
|
||||||
// Last used directory
|
// Last used directory
|
||||||
QString directory = settings_.value("add_folder_path", QDir::currentPath()).toString();
|
Settings s;
|
||||||
|
s.beginGroup(MainWindowSettings::kSettingsGroup);
|
||||||
|
QString directory = s.value("add_folder_path", QDir::currentPath()).toString();
|
||||||
|
|
||||||
// Show dialog
|
// Show dialog
|
||||||
directory = QFileDialog::getExistingDirectory(this, tr("Add folder"), directory);
|
directory = QFileDialog::getExistingDirectory(this, tr("Add folder"), directory);
|
||||||
if (directory.isEmpty()) return;
|
if (directory.isEmpty()) return;
|
||||||
|
|
||||||
// Save last used directory
|
// Save last used directory
|
||||||
settings_.setValue("add_folder_path", directory);
|
s.setValue("add_folder_path", directory);
|
||||||
|
|
||||||
// Add media
|
// Add media
|
||||||
MimeData *mimedata = new MimeData;
|
MimeData *mimedata = new MimeData;
|
||||||
@@ -3321,7 +3382,7 @@ void MainWindow::PlaylistDelete() {
|
|||||||
|
|
||||||
if (DeleteConfirmationDialog::warning(files) != QDialogButtonBox::Yes) return;
|
if (DeleteConfirmationDialog::warning(files) != QDialogButtonBox::Yes) return;
|
||||||
|
|
||||||
if (app_->player()->GetState() == EngineBase::State::Playing && app_->playlist_manager()->current()->rowCount() == selected_songs.count()) {
|
if (app_->player()->GetState() == EngineBase::State::Playing && app_->playlist_manager()->current() == app_->playlist_manager()->active() && app_->playlist_manager()->current()->rowCount() == selected_songs.count()) {
|
||||||
app_->player()->Stop();
|
app_->player()->Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3378,3 +3439,172 @@ void MainWindow::FocusSearchField() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::FetchStreamingMetadata() {
|
||||||
|
|
||||||
|
const QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
|
||||||
|
for (const QModelIndex &proxy_index : proxy_indexes) {
|
||||||
|
const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index);
|
||||||
|
if (!source_index.isValid()) continue;
|
||||||
|
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(source_index.row()));
|
||||||
|
if (!item) continue;
|
||||||
|
|
||||||
|
const Song &song = item->EffectiveMetadata();
|
||||||
|
const QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
|
||||||
|
QString track_id;
|
||||||
|
|
||||||
|
#ifdef HAVE_QOBUZ
|
||||||
|
if (song.source() == Song::Source::Qobuz) {
|
||||||
|
track_id = song.song_id();
|
||||||
|
// song_id() may be empty if not persisted, fall back to URL path
|
||||||
|
if (track_id.isEmpty()) {
|
||||||
|
track_id = song.url().path();
|
||||||
|
}
|
||||||
|
if (track_id.isEmpty()) {
|
||||||
|
qLog(Error) << "Failed to fetch Qobuz metadata: No track ID";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_SPOTIFY
|
||||||
|
if (song.source() == Song::Source::Spotify) {
|
||||||
|
track_id = song.song_id();
|
||||||
|
// song_id() may be empty if not persisted, fall back to parsing URL
|
||||||
|
if (track_id.isEmpty() && song.url().scheme() == "spotify"_L1 && song.url().path().startsWith(u"track:"_s)) {
|
||||||
|
track_id = song.url().path().mid(6);
|
||||||
|
}
|
||||||
|
if (track_id.isEmpty()) {
|
||||||
|
qLog(Error) << "Failed to fetch Spotify metadata: No track ID";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!track_id.isEmpty()) {
|
||||||
|
metadata_queue_.append({song.source(), track_id, persistent_index});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start processing the queue if it's not already running
|
||||||
|
if (!metadata_queue_.isEmpty() && !metadata_queue_timer_->isActive()) {
|
||||||
|
ProcessMetadataQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::ProcessMetadataQueue() {
|
||||||
|
|
||||||
|
if (metadata_queue_.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetadataQueueEntry metadata_queue_entry = metadata_queue_.takeFirst();
|
||||||
|
|
||||||
|
#ifdef HAVE_QOBUZ
|
||||||
|
if (metadata_queue_entry.source == Song::Source::Qobuz) {
|
||||||
|
if (QobuzServicePtr qobuz_service = app_->streaming_services()->Service<QobuzService>()) {
|
||||||
|
QobuzMetadataRequest *request = new QobuzMetadataRequest(&*qobuz_service, qobuz_service->network());
|
||||||
|
QObject::connect(request, &QobuzMetadataRequest::MetadataReceived, this, [this, metadata_queue_entry, request](const QString &received_track_id, const Song &fetched_song) {
|
||||||
|
Q_UNUSED(received_track_id);
|
||||||
|
if (metadata_queue_entry.persistent_index.isValid() && fetched_song.is_valid()) {
|
||||||
|
PlaylistItemPtr playlist_item = app_->playlist_manager()->current()->item_at(metadata_queue_entry.persistent_index.row());
|
||||||
|
if (playlist_item) {
|
||||||
|
const Song old_song = playlist_item->OriginalMetadata();
|
||||||
|
Song updated_song = old_song;
|
||||||
|
// Update all metadata fields from the fetched song
|
||||||
|
if (!fetched_song.title().isEmpty()) updated_song.set_title(fetched_song.title());
|
||||||
|
if (!fetched_song.artist().isEmpty()) updated_song.set_artist(fetched_song.artist());
|
||||||
|
if (!fetched_song.album().isEmpty()) updated_song.set_album(fetched_song.album());
|
||||||
|
if (!fetched_song.albumartist().isEmpty()) updated_song.set_albumartist(fetched_song.albumartist());
|
||||||
|
if (!fetched_song.genre().isEmpty()) updated_song.set_genre(fetched_song.genre());
|
||||||
|
if (!fetched_song.composer().isEmpty()) updated_song.set_composer(fetched_song.composer());
|
||||||
|
if (!fetched_song.performer().isEmpty()) updated_song.set_performer(fetched_song.performer());
|
||||||
|
if (!fetched_song.comment().isEmpty()) updated_song.set_comment(fetched_song.comment());
|
||||||
|
if (fetched_song.track() > 0) updated_song.set_track(fetched_song.track());
|
||||||
|
if (fetched_song.disc() > 0) updated_song.set_disc(fetched_song.disc());
|
||||||
|
if (fetched_song.year() > 0) updated_song.set_year(fetched_song.year());
|
||||||
|
if (fetched_song.length_nanosec() > 0) updated_song.set_length_nanosec(fetched_song.length_nanosec());
|
||||||
|
if (fetched_song.art_automatic().isValid()) updated_song.set_art_automatic(fetched_song.art_automatic());
|
||||||
|
playlist_item->SetOriginalMetadata(updated_song);
|
||||||
|
app_->playlist_manager()->current()->ItemReload(metadata_queue_entry.persistent_index, old_song, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request->deleteLater();
|
||||||
|
// Process next item in queue
|
||||||
|
if (!metadata_queue_.isEmpty()) {
|
||||||
|
metadata_queue_timer_->start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
QObject::connect(request, &QobuzMetadataRequest::MetadataFailure, this, [this, request](const QString &failed_track_id, const QString &error) {
|
||||||
|
Q_UNUSED(failed_track_id);
|
||||||
|
qLog(Error) << "Failed to fetch Qobuz metadata:" << error;
|
||||||
|
request->deleteLater();
|
||||||
|
// Process next item in queue
|
||||||
|
if (!metadata_queue_.isEmpty()) {
|
||||||
|
metadata_queue_timer_->start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
request->FetchTrackMetadata(metadata_queue_entry.track_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_SPOTIFY
|
||||||
|
if (metadata_queue_entry.source == Song::Source::Spotify) {
|
||||||
|
if (SpotifyServicePtr spotify_service = app_->streaming_services()->Service<SpotifyService>()) {
|
||||||
|
SpotifyMetadataRequest *request = new SpotifyMetadataRequest(&*spotify_service, app_->network());
|
||||||
|
QObject::connect(request, &SpotifyMetadataRequest::MetadataReceived, this, [this, metadata_queue_entry, request](const QString &received_track_id, const Song &fetched_song) {
|
||||||
|
Q_UNUSED(received_track_id);
|
||||||
|
if (metadata_queue_entry.persistent_index.isValid() && fetched_song.is_valid()) {
|
||||||
|
PlaylistItemPtr playlist_item = app_->playlist_manager()->current()->item_at(metadata_queue_entry.persistent_index.row());
|
||||||
|
if (playlist_item) {
|
||||||
|
const Song old_song = playlist_item->OriginalMetadata();
|
||||||
|
Song updated_song = old_song;
|
||||||
|
// Update all metadata fields from the fetched song
|
||||||
|
if (!fetched_song.title().isEmpty()) updated_song.set_title(fetched_song.title());
|
||||||
|
if (!fetched_song.artist().isEmpty()) updated_song.set_artist(fetched_song.artist());
|
||||||
|
if (!fetched_song.album().isEmpty()) updated_song.set_album(fetched_song.album());
|
||||||
|
if (!fetched_song.albumartist().isEmpty()) updated_song.set_albumartist(fetched_song.albumartist());
|
||||||
|
if (!fetched_song.genre().isEmpty()) updated_song.set_genre(fetched_song.genre());
|
||||||
|
if (!fetched_song.composer().isEmpty()) updated_song.set_composer(fetched_song.composer());
|
||||||
|
if (!fetched_song.performer().isEmpty()) updated_song.set_performer(fetched_song.performer());
|
||||||
|
if (!fetched_song.comment().isEmpty()) updated_song.set_comment(fetched_song.comment());
|
||||||
|
if (fetched_song.track() > 0) updated_song.set_track(fetched_song.track());
|
||||||
|
if (fetched_song.disc() > 0) updated_song.set_disc(fetched_song.disc());
|
||||||
|
if (fetched_song.year() > 0) updated_song.set_year(fetched_song.year());
|
||||||
|
if (fetched_song.length_nanosec() > 0) updated_song.set_length_nanosec(fetched_song.length_nanosec());
|
||||||
|
if (fetched_song.art_automatic().isValid()) updated_song.set_art_automatic(fetched_song.art_automatic());
|
||||||
|
playlist_item->SetOriginalMetadata(updated_song);
|
||||||
|
app_->playlist_manager()->current()->ItemReload(metadata_queue_entry.persistent_index, old_song, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request->deleteLater();
|
||||||
|
// Process next item in queue
|
||||||
|
if (!metadata_queue_.isEmpty()) {
|
||||||
|
metadata_queue_timer_->start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
QObject::connect(request, &SpotifyMetadataRequest::MetadataFailure, this, [this, request](const QString &failed_track_id, const QString &error) {
|
||||||
|
Q_UNUSED(failed_track_id);
|
||||||
|
qLog(Error) << "Failed to fetch Spotify metadata:" << error;
|
||||||
|
request->deleteLater();
|
||||||
|
// Process next item in queue
|
||||||
|
if (!metadata_queue_.isEmpty()) {
|
||||||
|
metadata_queue_timer_->start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
request->FetchTrackMetadata(metadata_queue_entry.track_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// If we get here, the source wasn't handled - try the next item
|
||||||
|
if (!metadata_queue_.isEmpty()) {
|
||||||
|
metadata_queue_timer_->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,9 +43,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QPixmap>
|
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QSettings>
|
|
||||||
#include <QtEvents>
|
#include <QtEvents>
|
||||||
|
|
||||||
#include "includes/scoped_ptr.h"
|
#include "includes/scoped_ptr.h"
|
||||||
@@ -53,7 +51,6 @@
|
|||||||
#include "includes/lazy.h"
|
#include "includes/lazy.h"
|
||||||
#include "core/platforminterface.h"
|
#include "core/platforminterface.h"
|
||||||
#include "core/song.h"
|
#include "core/song.h"
|
||||||
#include "core/settings.h"
|
|
||||||
#include "core/commandlineoptions.h"
|
#include "core/commandlineoptions.h"
|
||||||
#include "tagreader/tagreaderclient.h"
|
#include "tagreader/tagreaderclient.h"
|
||||||
#include "osd/osdbase.h"
|
#include "osd/osdbase.h"
|
||||||
@@ -248,7 +245,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
void ToggleSearchCoverAuto(const bool checked);
|
void ToggleSearchCoverAuto(const bool checked);
|
||||||
void SaveGeometry();
|
void SaveGeometry();
|
||||||
|
|
||||||
void Exit();
|
|
||||||
void DoExit();
|
void DoExit();
|
||||||
|
|
||||||
void HandleNotificationPreview(const OSDSettings::Type type, const QString &line1, const QString &line2);
|
void HandleNotificationPreview(const OSDSettings::Type type, const QString &line1, const QString &line2);
|
||||||
@@ -280,12 +276,15 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
|
|
||||||
void DeleteFilesFinished(const SongList &songs_with_errors);
|
void DeleteFilesFinished(const SongList &songs_with_errors);
|
||||||
|
|
||||||
|
void FetchStreamingMetadata();
|
||||||
|
void ProcessMetadataQueue();
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void CommandlineOptionsReceived(const QByteArray &string_options);
|
void CommandlineOptionsReceived(const QByteArray &string_options);
|
||||||
void Raise();
|
void Raise();
|
||||||
|
void Exit();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
void SaveSettings();
|
void SaveSettings();
|
||||||
|
|
||||||
static void ApplyAddBehaviour(const BehaviourSettings::AddBehaviour b, MimeData *mimedata);
|
static void ApplyAddBehaviour(const BehaviourSettings::AddBehaviour b, MimeData *mimedata);
|
||||||
@@ -293,9 +292,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
|
|
||||||
void CheckFullRescanRevisions();
|
void CheckFullRescanRevisions();
|
||||||
|
|
||||||
// creates the icon by painting the full one depending on the current position
|
|
||||||
QPixmap CreateOverlayedIcon(const int position, const int scrobble_point);
|
|
||||||
|
|
||||||
void GetCoverAutomatically();
|
void GetCoverAutomatically();
|
||||||
|
|
||||||
void SetToggleScrobblingIcon(const bool value);
|
void SetToggleScrobblingIcon(const bool value);
|
||||||
@@ -386,12 +382,13 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
QList<QAction*> playlistitem_actions_;
|
QList<QAction*> playlistitem_actions_;
|
||||||
QAction *playlistitem_actions_separator_;
|
QAction *playlistitem_actions_separator_;
|
||||||
QAction *playlist_rescan_songs_;
|
QAction *playlist_rescan_songs_;
|
||||||
|
QAction *playlist_fetch_metadata_;
|
||||||
|
|
||||||
QModelIndex playlist_menu_index_;
|
QModelIndex playlist_menu_index_;
|
||||||
|
|
||||||
QTimer *track_position_timer_;
|
QTimer *track_position_timer_;
|
||||||
QTimer *track_slider_timer_;
|
QTimer *track_slider_timer_;
|
||||||
Settings settings_;
|
QTimer *metadata_queue_timer_;
|
||||||
|
|
||||||
bool keep_running_;
|
bool keep_running_;
|
||||||
bool playing_widget_;
|
bool playing_widget_;
|
||||||
@@ -416,6 +413,13 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
bool delete_files_;
|
bool delete_files_;
|
||||||
std::optional<CommandlineOptions> options_;
|
std::optional<CommandlineOptions> options_;
|
||||||
|
|
||||||
|
class MetadataQueueEntry {
|
||||||
|
public:
|
||||||
|
Song::Source source;
|
||||||
|
QString track_id;
|
||||||
|
QPersistentModelIndex persistent_index;
|
||||||
|
};
|
||||||
|
QList<MetadataQueueEntry> metadata_queue_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // MAINWINDOW_H
|
#endif // MAINWINDOW_H
|
||||||
|
|||||||
@@ -34,6 +34,13 @@
|
|||||||
|
|
||||||
#include "mergedproxymodel.h"
|
#include "mergedproxymodel.h"
|
||||||
|
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#if __GNUC__ >= 16
|
||||||
|
#pragma GCC diagnostic ignored "-Wstringop-overflow"
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <boost/multi_index/detail/bidir_node_iterator.hpp>
|
#include <boost/multi_index/detail/bidir_node_iterator.hpp>
|
||||||
#include <boost/multi_index/detail/hash_index_iterator.hpp>
|
#include <boost/multi_index/detail/hash_index_iterator.hpp>
|
||||||
#include <boost/multi_index/hashed_index.hpp>
|
#include <boost/multi_index/hashed_index.hpp>
|
||||||
@@ -45,6 +52,10 @@
|
|||||||
#include <boost/multi_index_container.hpp>
|
#include <boost/multi_index_container.hpp>
|
||||||
#include <boost/operators.hpp>
|
#include <boost/operators.hpp>
|
||||||
|
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
#endif
|
||||||
|
|
||||||
using boost::multi_index::hashed_unique;
|
using boost::multi_index::hashed_unique;
|
||||||
using boost::multi_index::identity;
|
using boost::multi_index::identity;
|
||||||
using boost::multi_index::indexed_by;
|
using boost::multi_index::indexed_by;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkInformation>
|
||||||
|
|
||||||
#include "networkaccessmanager.h"
|
#include "networkaccessmanager.h"
|
||||||
#include "threadsafenetworkdiskcache.h"
|
#include "threadsafenetworkdiskcache.h"
|
||||||
@@ -41,6 +42,22 @@ NetworkAccessManager::NetworkAccessManager(QObject *parent)
|
|||||||
setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
setCache(new ThreadSafeNetworkDiskCache(this));
|
setCache(new ThreadSafeNetworkDiskCache(this));
|
||||||
|
|
||||||
|
// Handle network state changes after system suspend/resume
|
||||||
|
// QNetworkInformation provides cross-platform network reachability monitoring in Qt 6
|
||||||
|
if (QNetworkInformation::loadDefaultBackend()) {
|
||||||
|
QNetworkInformation *network_info = QNetworkInformation::instance();
|
||||||
|
if (network_info) {
|
||||||
|
QObject::connect(network_info, &QNetworkInformation::reachabilityChanged, this, [this](QNetworkInformation::Reachability reachability) {
|
||||||
|
if (reachability == QNetworkInformation::Reachability::Online) {
|
||||||
|
// Clear connection cache to force reconnection after network becomes available
|
||||||
|
// This fixes issues after system suspend/resume
|
||||||
|
clearConnectionCache();
|
||||||
|
clearAccessCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) {
|
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) {
|
||||||
@@ -50,7 +67,7 @@ QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkR
|
|||||||
user_agent = network_request.header(QNetworkRequest::UserAgentHeader).toByteArray();
|
user_agent = network_request.header(QNetworkRequest::UserAgentHeader).toByteArray();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
user_agent = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
|
user_agent = "Strawberry Music Player";
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkRequest new_network_request(network_request);
|
QNetworkRequest new_network_request(network_request);
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ class Player : public PlayerInterface {
|
|||||||
void PlayPlaylistInternal(const EngineBase::TrackChangeFlags, const Playlist::AutoScroll autoscroll, const QString &playlist_name);
|
void PlayPlaylistInternal(const EngineBase::TrackChangeFlags, const Playlist::AutoScroll autoscroll, const QString &playlist_name);
|
||||||
|
|
||||||
void FatalError();
|
void FatalError();
|
||||||
void ValidSongRequested(const QUrl&);
|
void ValidSongRequested(const QUrl &url);
|
||||||
void InvalidSongRequested(const QUrl&);
|
void InvalidSongRequested(const QUrl &url);
|
||||||
|
|
||||||
void HandleLoadResult(const UrlHandler::LoadResult &result);
|
void HandleLoadResult(const UrlHandler::LoadResult &result);
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class QtFSListener : public FileSystemWatcherInterface {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
QFileSystemWatcher watcher_;
|
QFileSystemWatcher watcher_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // QTFSLISTENER_H
|
#endif // QTFSLISTENER_H
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class ScopedNSAutoreleasePool {
|
|||||||
// Only use then when you're certain the items currently in the pool are
|
// Only use then when you're certain the items currently in the pool are
|
||||||
// no longer needed.
|
// no longer needed.
|
||||||
void Recycle();
|
void Recycle();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
NSAutoreleasePool *autorelease_pool_;
|
NSAutoreleasePool *autorelease_pool_;
|
||||||
|
|
||||||
|
|||||||
@@ -353,6 +353,8 @@ struct Song::Private : public QSharedData {
|
|||||||
std::optional<double> ebur128_integrated_loudness_lufs_;
|
std::optional<double> ebur128_integrated_loudness_lufs_;
|
||||||
std::optional<double> ebur128_loudness_range_lu_;
|
std::optional<double> ebur128_loudness_range_lu_;
|
||||||
|
|
||||||
|
int id3v2_version_; // ID3v2 tag version (3 or 4), 0 if not applicable or unknown
|
||||||
|
|
||||||
bool init_from_file_; // Whether this song was loaded from a file using taglib.
|
bool init_from_file_; // Whether this song was loaded from a file using taglib.
|
||||||
bool suspicious_tags_; // Whether our encoding guesser thinks these tags might be incorrectly encoded.
|
bool suspicious_tags_; // Whether our encoding guesser thinks these tags might be incorrectly encoded.
|
||||||
|
|
||||||
@@ -400,6 +402,8 @@ Song::Private::Private(const Source source)
|
|||||||
rating_(-1),
|
rating_(-1),
|
||||||
bpm_(-1),
|
bpm_(-1),
|
||||||
|
|
||||||
|
id3v2_version_(0),
|
||||||
|
|
||||||
init_from_file_(false),
|
init_from_file_(false),
|
||||||
suspicious_tags_(false)
|
suspicious_tags_(false)
|
||||||
|
|
||||||
@@ -510,6 +514,8 @@ const QString &Song::musicbrainz_work_id() const { return d->musicbrainz_work_id
|
|||||||
std::optional<double> Song::ebur128_integrated_loudness_lufs() const { return d->ebur128_integrated_loudness_lufs_; }
|
std::optional<double> Song::ebur128_integrated_loudness_lufs() const { return d->ebur128_integrated_loudness_lufs_; }
|
||||||
std::optional<double> Song::ebur128_loudness_range_lu() const { return d->ebur128_loudness_range_lu_; }
|
std::optional<double> Song::ebur128_loudness_range_lu() const { return d->ebur128_loudness_range_lu_; }
|
||||||
|
|
||||||
|
int Song::id3v2_version() const { return d->id3v2_version_; }
|
||||||
|
|
||||||
QString *Song::mutable_title() { return &d->title_; }
|
QString *Song::mutable_title() { return &d->title_; }
|
||||||
QString *Song::mutable_album() { return &d->album_; }
|
QString *Song::mutable_album() { return &d->album_; }
|
||||||
QString *Song::mutable_artist() { return &d->artist_; }
|
QString *Song::mutable_artist() { return &d->artist_; }
|
||||||
@@ -624,6 +630,8 @@ void Song::set_musicbrainz_work_id(const QString &v) { d->musicbrainz_work_id_ =
|
|||||||
void Song::set_ebur128_integrated_loudness_lufs(const std::optional<double> v) { d->ebur128_integrated_loudness_lufs_ = v; }
|
void Song::set_ebur128_integrated_loudness_lufs(const std::optional<double> v) { d->ebur128_integrated_loudness_lufs_ = v; }
|
||||||
void Song::set_ebur128_loudness_range_lu(const std::optional<double> v) { d->ebur128_loudness_range_lu_ = v; }
|
void Song::set_ebur128_loudness_range_lu(const std::optional<double> v) { d->ebur128_loudness_range_lu_ = v; }
|
||||||
|
|
||||||
|
void Song::set_id3v2_version(const int v) { d->id3v2_version_ = v; }
|
||||||
|
|
||||||
void Song::set_init_from_file(const bool v) { d->init_from_file_ = v; }
|
void Song::set_init_from_file(const bool v) { d->init_from_file_ = v; }
|
||||||
|
|
||||||
void Song::set_stream_url(const QUrl &v) { d->stream_url_ = v; }
|
void Song::set_stream_url(const QUrl &v) { d->stream_url_ = v; }
|
||||||
@@ -678,11 +686,12 @@ const QString &Song::playlist_effective_albumartistsort() const { return is_comp
|
|||||||
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
|
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
|
||||||
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
|
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
|
||||||
bool Song::is_linked_collection_song() const { return IsLinkedCollectionSource(d->source_); }
|
bool Song::is_linked_collection_song() const { return IsLinkedCollectionSource(d->source_); }
|
||||||
bool Song::is_stream() const { return is_radio() || d->source_ == Source::Tidal || d->source_ == Source::Subsonic || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
|
|
||||||
bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; }
|
bool Song::is_radio() const { return d->source_ == Source::Stream || d->source_ == Source::SomaFM || d->source_ == Source::RadioParadise; }
|
||||||
|
bool Song::is_stream_service() const { return d->source_ == Source::Subsonic || d->source_ == Source::Tidal || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
|
||||||
|
bool Song::is_stream() const { return is_radio() || is_stream_service(); }
|
||||||
bool Song::is_cdda() const { return d->source_ == Source::CDDA; }
|
bool Song::is_cdda() const { return d->source_ == Source::CDDA; }
|
||||||
bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; }
|
bool Song::is_compilation() const { return (d->compilation_ || d->compilation_detected_ || d->compilation_on_) && !d->compilation_off_; }
|
||||||
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz || d->source_ == Source::Spotify; }
|
bool Song::stream_url_can_expire() const { return d->source_ == Source::Tidal || d->source_ == Source::Qobuz; }
|
||||||
bool Song::is_module_music() const { return d->filetype_ == FileType::MOD || d->filetype_ == FileType::S3M || d->filetype_ == FileType::XM || d->filetype_ == FileType::IT; }
|
bool Song::is_module_music() const { return d->filetype_ == FileType::MOD || d->filetype_ == FileType::S3M || d->filetype_ == FileType::XM || d->filetype_ == FileType::IT; }
|
||||||
bool Song::has_cue() const { return !d->cue_path_.isEmpty(); }
|
bool Song::has_cue() const { return !d->cue_path_.isEmpty(); }
|
||||||
|
|
||||||
@@ -797,19 +806,19 @@ bool Song::lyrics_supported() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Song::albumartistsort_supported() const {
|
bool Song::albumartistsort_supported() const {
|
||||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
|
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::OggOpus || d->filetype_ == FileType::MPEG;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Song::albumsort_supported() const {
|
bool Song::albumsort_supported() const {
|
||||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
|
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::OggOpus || d->filetype_ == FileType::MPEG;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Song::artistsort_supported() const {
|
bool Song::artistsort_supported() const {
|
||||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
|
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::OggOpus || d->filetype_ == FileType::MPEG;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Song::composersort_supported() const {
|
bool Song::composersort_supported() const {
|
||||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
|
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::OggOpus || d->filetype_ == FileType::MPEG;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Song::performersort_supported() const {
|
bool Song::performersort_supported() const {
|
||||||
@@ -818,7 +827,7 @@ bool Song::performersort_supported() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Song::titlesort_supported() const {
|
bool Song::titlesort_supported() const {
|
||||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::MPEG;
|
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::OggOpus || d->filetype_ == FileType::MPEG;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Song::save_embedded_cover_supported(const FileType filetype) {
|
bool Song::save_embedded_cover_supported(const FileType filetype) {
|
||||||
@@ -833,6 +842,10 @@ bool Song::save_embedded_cover_supported(const FileType filetype) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Song::id3v2_tags_supported() const {
|
||||||
|
return d->filetype_ == FileType::MPEG || d->filetype_ == FileType::WAV || d->filetype_ == FileType::AIFF;
|
||||||
|
}
|
||||||
|
|
||||||
int Song::ColumnIndex(const QString &field) {
|
int Song::ColumnIndex(const QString &field) {
|
||||||
|
|
||||||
return static_cast<int>(kRowIdColumns.indexOf(field));
|
return static_cast<int>(kRowIdColumns.indexOf(field));
|
||||||
@@ -944,7 +957,7 @@ QString Song::PrettyRating() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Song::IsEditable() const {
|
bool Song::IsEditable() const {
|
||||||
return d->valid_ && d->url_.isValid() && ((d->url_.isLocalFile() && write_tags_supported() && !has_cue()) || d->source_ == Source::Stream);
|
return d->valid_ && d->url_.isValid() && ((d->url_.isLocalFile() && write_tags_supported() && !has_cue()) || is_stream());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Song::IsFileInfoEqual(const Song &other) const {
|
bool Song::IsFileInfoEqual(const Song &other) const {
|
||||||
@@ -1656,12 +1669,24 @@ void Song::InitArtManual() {
|
|||||||
void Song::InitArtAutomatic() {
|
void Song::InitArtAutomatic() {
|
||||||
|
|
||||||
if (d->art_automatic_.isEmpty() && d->source_ == Source::LocalFile && d->url_.isLocalFile()) {
|
if (d->art_automatic_.isEmpty() && d->source_ == Source::LocalFile && d->url_.isLocalFile()) {
|
||||||
// Pick the first image file in the album directory.
|
const QFileInfo fileinfo(d->url_.toLocalFile());
|
||||||
QFileInfo file(d->url_.toLocalFile());
|
const QDir dir(fileinfo.path());
|
||||||
QDir dir(file.path());
|
const QStringList cover_files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
|
||||||
QStringList files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
|
QString best_cover_file;
|
||||||
if (files.count() > 0) {
|
for (const QString &cover_file : cover_files) {
|
||||||
d->art_automatic_ = QUrl::fromLocalFile(file.path() + QDir::separator() + files.first());
|
if (cover_file.contains("back"_L1, Qt::CaseInsensitive)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cover_file.contains("front"_L1, Qt::CaseInsensitive) || cover_file.startsWith("cover"_L1, Qt::CaseInsensitive)) {
|
||||||
|
best_cover_file = cover_file;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (best_cover_file.isEmpty()) {
|
||||||
|
best_cover_file = cover_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!best_cover_file.isEmpty()) {
|
||||||
|
d->art_automatic_ = QUrl::fromLocalFile(fileinfo.path() + QDir::separator() + best_cover_file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2150,4 +2175,3 @@ QString Song::GetNameForNewPlaylist(const SongList &songs) {
|
|||||||
return result;
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ class Song {
|
|||||||
std::optional<double> ebur128_integrated_loudness_lufs() const;
|
std::optional<double> ebur128_integrated_loudness_lufs() const;
|
||||||
std::optional<double> ebur128_loudness_range_lu() const;
|
std::optional<double> ebur128_loudness_range_lu() const;
|
||||||
|
|
||||||
|
int id3v2_version() const;
|
||||||
|
|
||||||
QString *mutable_title();
|
QString *mutable_title();
|
||||||
QString *mutable_album();
|
QString *mutable_album();
|
||||||
QString *mutable_artist();
|
QString *mutable_artist();
|
||||||
@@ -349,6 +351,8 @@ class Song {
|
|||||||
void set_ebur128_integrated_loudness_lufs(const std::optional<double> v);
|
void set_ebur128_integrated_loudness_lufs(const std::optional<double> v);
|
||||||
void set_ebur128_loudness_range_lu(const std::optional<double> v);
|
void set_ebur128_loudness_range_lu(const std::optional<double> v);
|
||||||
|
|
||||||
|
void set_id3v2_version(const int v);
|
||||||
|
|
||||||
void set_init_from_file(const bool v);
|
void set_init_from_file(const bool v);
|
||||||
|
|
||||||
void set_stream_url(const QUrl &v);
|
void set_stream_url(const QUrl &v);
|
||||||
@@ -403,8 +407,9 @@ class Song {
|
|||||||
bool is_metadata_good() const;
|
bool is_metadata_good() const;
|
||||||
bool is_local_collection_song() const;
|
bool is_local_collection_song() const;
|
||||||
bool is_linked_collection_song() const;
|
bool is_linked_collection_song() const;
|
||||||
bool is_stream() const;
|
|
||||||
bool is_radio() const;
|
bool is_radio() const;
|
||||||
|
bool is_stream_service() const;
|
||||||
|
bool is_stream() const;
|
||||||
bool is_cdda() const;
|
bool is_cdda() const;
|
||||||
bool is_compilation() const;
|
bool is_compilation() const;
|
||||||
bool stream_url_can_expire() const;
|
bool stream_url_can_expire() const;
|
||||||
@@ -439,6 +444,8 @@ class Song {
|
|||||||
static bool save_embedded_cover_supported(const FileType filetype);
|
static bool save_embedded_cover_supported(const FileType filetype);
|
||||||
bool save_embedded_cover_supported() const { return url().isLocalFile() && save_embedded_cover_supported(filetype()) && !has_cue(); };
|
bool save_embedded_cover_supported() const { return url().isLocalFile() && save_embedded_cover_supported(filetype()) && !has_cue(); };
|
||||||
|
|
||||||
|
bool id3v2_tags_supported() const;
|
||||||
|
|
||||||
static int ColumnIndex(const QString &field);
|
static int ColumnIndex(const QString &field);
|
||||||
static QString JoinSpec(const QString &table);
|
static QString JoinSpec(const QString &table);
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ SongLoader::SongLoader(const SharedPtr<UrlHandlers> url_handlers,
|
|||||||
|
|
||||||
QObject::connect(timeout_timer_, &QTimer::timeout, this, &SongLoader::Timeout);
|
QObject::connect(timeout_timer_, &QTimer::timeout, this, &SongLoader::Timeout);
|
||||||
|
|
||||||
|
QObject::connect(playlist_parser_, &PlaylistParser::Error, this, &SongLoader::ParserError);
|
||||||
|
QObject::connect(cue_parser_, &CueParser::Error, this, &SongLoader::ParserError);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SongLoader::~SongLoader() {
|
SongLoader::~SongLoader() {
|
||||||
@@ -106,6 +109,10 @@ SongLoader::~SongLoader() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SongLoader::ParserError(const QString &error) {
|
||||||
|
errors_ << error;
|
||||||
|
}
|
||||||
|
|
||||||
SongLoader::Result SongLoader::Load(const QUrl &url) {
|
SongLoader::Result SongLoader::Load(const QUrl &url) {
|
||||||
|
|
||||||
if (url.isEmpty()) return Result::Error;
|
if (url.isEmpty()) return Result::Error;
|
||||||
@@ -287,6 +294,7 @@ SongLoader::Result SongLoader::LoadLocalAsync(const QString &filename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parser) { // It's a playlist!
|
if (parser) { // It's a playlist!
|
||||||
|
QObject::connect(parser, &ParserBase::Error, this, &SongLoader::ParserError, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
|
||||||
qLog(Debug) << "Parsing using" << parser->name();
|
qLog(Debug) << "Parsing using" << parser->name();
|
||||||
LoadPlaylist(parser, filename);
|
LoadPlaylist(parser, filename);
|
||||||
return Result::Success;
|
return Result::Success;
|
||||||
@@ -706,6 +714,10 @@ void SongLoader::MagicReady() {
|
|||||||
StopTypefindAsync(true);
|
StopTypefindAsync(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parser_) {
|
||||||
|
QObject::connect(parser_, &ParserBase::Error, this, &SongLoader::ParserError, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
|
||||||
|
}
|
||||||
|
|
||||||
state_ = State::WaitingForData;
|
state_ = State::WaitingForData;
|
||||||
|
|
||||||
if (!IsPipelinePlaying()) {
|
if (!IsPipelinePlaying()) {
|
||||||
@@ -784,4 +796,3 @@ void SongLoader::CleanupPipeline() {
|
|||||||
state_ = State::Finished;
|
state_ = State::Finished;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class SongLoader : public QObject {
|
|||||||
void ScheduleTimeout();
|
void ScheduleTimeout();
|
||||||
void Timeout();
|
void Timeout();
|
||||||
void StopTypefind();
|
void StopTypefind();
|
||||||
|
void ParserError(const QString &error);
|
||||||
|
|
||||||
#ifdef HAVE_AUDIOCD
|
#ifdef HAVE_AUDIOCD
|
||||||
void AudioCDTracksLoadErrorSlot(const QString &error);
|
void AudioCDTracksLoadErrorSlot(const QString &error);
|
||||||
@@ -171,7 +172,6 @@ class SongLoader : public QObject {
|
|||||||
QStringList errors_;
|
QStringList errors_;
|
||||||
|
|
||||||
bool success_;
|
bool success_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SONGLOADER_H
|
#endif // SONGLOADER_H
|
||||||
|
|||||||
@@ -41,4 +41,3 @@ class SongMimeData : public MimeData {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // SONGMIMEDATA_H
|
#endif // SONGMIMEDATA_H
|
||||||
|
|
||||||
|
|||||||
175
src/core/unixsignalwatcher.cpp
Normal file
175
src/core/unixsignalwatcher.cpp
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 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
|
||||||
|
* 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/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <csignal>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <fcntl.h>
|
||||||
|
|
||||||
|
#include <QSocketNotifier>
|
||||||
|
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "unixsignalwatcher.h"
|
||||||
|
|
||||||
|
UnixSignalWatcher *UnixSignalWatcher::sInstance = nullptr;
|
||||||
|
|
||||||
|
UnixSignalWatcher::UnixSignalWatcher(QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
signal_fd_{-1, -1},
|
||||||
|
socket_notifier_(nullptr) {
|
||||||
|
|
||||||
|
Q_ASSERT(!sInstance);
|
||||||
|
|
||||||
|
// Create a socket pair for the self-pipe trick
|
||||||
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, signal_fd_) != 0) {
|
||||||
|
qLog(Error) << "Failed to create socket pair for signal handling:" << ::strerror(errno);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(signal_fd_[0] != -1);
|
||||||
|
|
||||||
|
// Set the read end to non-blocking mode
|
||||||
|
// Non-blocking mode is important to prevent HandleSignalNotification from blocking
|
||||||
|
int flags = ::fcntl(signal_fd_[0], F_GETFL, 0);
|
||||||
|
if (flags == -1) {
|
||||||
|
qLog(Error) << "Failed to get socket flags:" << ::strerror(errno);
|
||||||
|
}
|
||||||
|
else if (::fcntl(signal_fd_[0], F_SETFL, flags | O_NONBLOCK) == -1) {
|
||||||
|
qLog(Error) << "Failed to set socket to non-blocking:" << ::strerror(errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the write end to non-blocking mode as well (used in signal handler)
|
||||||
|
// Non-blocking mode prevents the signal handler from blocking if buffer is full
|
||||||
|
flags = ::fcntl(signal_fd_[1], F_GETFL, 0);
|
||||||
|
if (flags == -1) {
|
||||||
|
qLog(Error) << "Failed to get socket flags for write end:" << ::strerror(errno);
|
||||||
|
}
|
||||||
|
else if (::fcntl(signal_fd_[1], F_SETFL, flags | O_NONBLOCK) == -1) {
|
||||||
|
qLog(Error) << "Failed to set write end of socket to non-blocking:" << ::strerror(errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up QSocketNotifier to monitor the read end of the socket
|
||||||
|
socket_notifier_ = new QSocketNotifier(signal_fd_[0], QSocketNotifier::Read, this);
|
||||||
|
QObject::connect(socket_notifier_, &QSocketNotifier::activated, this, &UnixSignalWatcher::HandleSignalNotification);
|
||||||
|
|
||||||
|
sInstance = this;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
UnixSignalWatcher::~UnixSignalWatcher() {
|
||||||
|
|
||||||
|
if (socket_notifier_) {
|
||||||
|
socket_notifier_->setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original signal handlers
|
||||||
|
for (int i = 0; i < watched_signals_.size(); ++i) {
|
||||||
|
if (::sigaction(watched_signals_[i], &original_signal_actions_[i], nullptr) != 0) {
|
||||||
|
qLog(Error) << "Failed to restore signal handler for signal" << watched_signals_[i] << ":" << ::strerror(errno);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signal_fd_[0] != -1) {
|
||||||
|
::close(signal_fd_[0]);
|
||||||
|
signal_fd_[0] = -1;
|
||||||
|
}
|
||||||
|
if (signal_fd_[1] != -1) {
|
||||||
|
::close(signal_fd_[1]);
|
||||||
|
signal_fd_[1] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sInstance = nullptr;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnixSignalWatcher::WatchForSignal(const int signal) {
|
||||||
|
|
||||||
|
// Check if socket pair was created successfully
|
||||||
|
if (signal_fd_[0] == -1 || signal_fd_[1] == -1) {
|
||||||
|
qLog(Error) << "Cannot watch for signal: socket pair not initialized";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watched_signals_.contains(signal)) {
|
||||||
|
qLog(Error) << "Already watching for signal" << signal;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sigaction signal_action{};
|
||||||
|
::memset(&signal_action, 0, sizeof(signal_action));
|
||||||
|
sigemptyset(&signal_action.sa_mask);
|
||||||
|
signal_action.sa_handler = UnixSignalWatcher::SignalHandler;
|
||||||
|
signal_action.sa_flags = SA_RESTART;
|
||||||
|
|
||||||
|
struct sigaction old_signal_action{};
|
||||||
|
::memset(&old_signal_action, 0, sizeof(old_signal_action));
|
||||||
|
if (::sigaction(signal, &signal_action, &old_signal_action) != 0) {
|
||||||
|
qLog(Error) << "sigaction error:" << ::strerror(errno);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
watched_signals_ << signal;
|
||||||
|
original_signal_actions_ << old_signal_action;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnixSignalWatcher::SignalHandler(const int signal) {
|
||||||
|
|
||||||
|
if (!sInstance || sInstance->signal_fd_[1] == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the signal number to the socket pair (async-signal-safe)
|
||||||
|
// This is the only operation we perform in the signal handler
|
||||||
|
// Ignore errors as there's nothing we can safely do about them in a signal handler
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Wunused-result"
|
||||||
|
#endif
|
||||||
|
(void)::write(sInstance->signal_fd_[1], &signal, sizeof(signal));
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
#endif
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnixSignalWatcher::HandleSignalNotification() {
|
||||||
|
|
||||||
|
// Read all pending signals from the socket
|
||||||
|
// Multiple signals could arrive before the notifier triggers
|
||||||
|
while (true) {
|
||||||
|
int signal = 0;
|
||||||
|
const ssize_t bytes_read = ::read(signal_fd_[0], &signal, sizeof(signal));
|
||||||
|
if (bytes_read == sizeof(signal)) {
|
||||||
|
qLog(Debug) << "Caught signal:" << signal;
|
||||||
|
Q_EMIT UnixSignal(signal);
|
||||||
|
}
|
||||||
|
else if (bytes_read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||||
|
// No more data available (expected with non-blocking socket)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Error occurred or partial read
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
53
src/core/unixsignalwatcher.h
Normal file
53
src/core/unixsignalwatcher.h
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 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
|
||||||
|
* 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 UNIXSIGNALWATCHER_H
|
||||||
|
#define UNIXSIGNALWATCHER_H
|
||||||
|
|
||||||
|
#include <csignal>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QList>
|
||||||
|
|
||||||
|
class QSocketNotifier;
|
||||||
|
|
||||||
|
class UnixSignalWatcher : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit UnixSignalWatcher(QObject *parent = nullptr);
|
||||||
|
~UnixSignalWatcher() override;
|
||||||
|
|
||||||
|
void WatchForSignal(const int signal);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void UnixSignal(const int signal);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void SignalHandler(const int signal);
|
||||||
|
void HandleSignalNotification();
|
||||||
|
|
||||||
|
static UnixSignalWatcher *sInstance;
|
||||||
|
int signal_fd_[2];
|
||||||
|
QSocketNotifier *socket_notifier_;
|
||||||
|
QList<int> watched_signals_;
|
||||||
|
QList<struct sigaction> original_signal_actions_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // UNIXSIGNALWATCHER_H
|
||||||
@@ -53,4 +53,3 @@ UrlHandler::LoadResult::LoadResult(const QUrl &media_url, const Type type, const
|
|||||||
bit_depth_(-1),
|
bit_depth_(-1),
|
||||||
length_nanosec_(-1),
|
length_nanosec_(-1),
|
||||||
error_(error) {}
|
error_(error) {}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ class UrlHandler : public QObject {
|
|||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void AsyncLoadComplete(const UrlHandler::LoadResult &result);
|
void AsyncLoadComplete(const UrlHandler::LoadResult &result);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // URLHANDLER_H
|
#endif // URLHANDLER_H
|
||||||
|
|||||||
@@ -812,8 +812,8 @@ QUrl AlbumCoverChoiceController::SaveCoverAutomatic(Song *song, const AlbumCover
|
|||||||
SaveCoverEmbeddedToCollectionSongs(*song, result);
|
SaveCoverEmbeddedToCollectionSongs(*song, result);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
[[fallthrough]];
|
[[fallthrough]];
|
||||||
|
}
|
||||||
case CoverOptions::CoverType::Cache:
|
case CoverOptions::CoverType::Cache:
|
||||||
case CoverOptions::CoverType::Album:{
|
case CoverOptions::CoverType::Album:{
|
||||||
cover_url = SaveCoverToFileAutomatic(song, result);
|
cover_url = SaveCoverToFileAutomatic(song, result);
|
||||||
|
|||||||
@@ -75,4 +75,3 @@ class AlbumCoverExporter : public QObject {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // ALBUMCOVEREXPORTER_H
|
#endif // ALBUMCOVEREXPORTER_H
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ class AlbumCoverFetcherSearch : public QObject {
|
|||||||
SharedPtr<NetworkAccessManager> network_;
|
SharedPtr<NetworkAccessManager> network_;
|
||||||
|
|
||||||
bool cancel_requested_;
|
bool cancel_requested_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // ALBUMCOVERFETCHERSEARCH_H
|
#endif // ALBUMCOVERFETCHERSEARCH_H
|
||||||
|
|||||||
@@ -58,5 +58,4 @@ AlbumCoverLoaderOptions::Types AlbumCoverLoaderOptions::LoadTypes() {
|
|||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
return cover_types;
|
return cover_types;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ class AlbumCoverLoaderResult {
|
|||||||
const AlbumCoverImageResult &_album_cover = AlbumCoverImageResult(),
|
const AlbumCoverImageResult &_album_cover = AlbumCoverImageResult(),
|
||||||
const QImage &_image_scaled = QImage(),
|
const QImage &_image_scaled = QImage(),
|
||||||
const QUrl &_art_manual_updated = QUrl(),
|
const QUrl &_art_manual_updated = QUrl(),
|
||||||
const QUrl &_art_automatic_updated = QUrl()) :
|
const QUrl &_art_automatic_updated = QUrl())
|
||||||
success(_success),
|
: success(_success),
|
||||||
type(_type),
|
type(_type),
|
||||||
album_cover(_album_cover),
|
album_cover(_album_cover),
|
||||||
image_scaled(_image_scaled),
|
image_scaled(_image_scaled),
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ using std::make_shared;
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr char kSettingsGroup[] = "CoverManager";
|
constexpr char kSettingsGroup[] = "CoverManager";
|
||||||
constexpr int kThumbnailSize = 120;
|
constexpr int kThumbnailSize = 120;
|
||||||
}
|
} // namespace
|
||||||
|
|
||||||
AlbumCoverManager::AlbumCoverManager(const SharedPtr<NetworkAccessManager> network,
|
AlbumCoverManager::AlbumCoverManager(const SharedPtr<NetworkAccessManager> network,
|
||||||
const SharedPtr<CollectionBackend> collection_backend,
|
const SharedPtr<CollectionBackend> collection_backend,
|
||||||
@@ -604,10 +604,7 @@ void AlbumCoverManager::AlbumCoverFetched(const quint64 id, const AlbumCoverImag
|
|||||||
|
|
||||||
void AlbumCoverManager::UpdateStatusText() {
|
void AlbumCoverManager::UpdateStatusText() {
|
||||||
|
|
||||||
QString message = tr("Got %1 covers out of %2 (%3 failed)")
|
QString message = tr("Got %1 covers out of %2 (%3 failed)").arg(fetch_statistics_.chosen_images_).arg(jobs_).arg(fetch_statistics_.missing_images_);
|
||||||
.arg(fetch_statistics_.chosen_images_)
|
|
||||||
.arg(jobs_)
|
|
||||||
.arg(fetch_statistics_.missing_images_);
|
|
||||||
|
|
||||||
if (fetch_statistics_.bytes_transferred_ > 0) {
|
if (fetch_statistics_.bytes_transferred_ > 0) {
|
||||||
message += ", "_L1 + tr("%1 transferred").arg(Utilities::PrettySize(fetch_statistics_.bytes_transferred_));
|
message += ", "_L1 + tr("%1 transferred").arg(Utilities::PrettySize(fetch_statistics_.bytes_transferred_));
|
||||||
@@ -1083,10 +1080,7 @@ void AlbumCoverManager::UpdateExportStatus(const int exported, const int skipped
|
|||||||
|
|
||||||
progress_bar_->setValue(exported);
|
progress_bar_->setValue(exported);
|
||||||
|
|
||||||
QString message = tr("Exported %1 covers out of %2 (%3 skipped)")
|
QString message = tr("Exported %1 covers out of %2 (%3 skipped)").arg(exported).arg(max).arg(skipped);
|
||||||
.arg(exported)
|
|
||||||
.arg(max)
|
|
||||||
.arg(skipped);
|
|
||||||
statusBar()->showMessage(message);
|
statusBar()->showMessage(message);
|
||||||
|
|
||||||
// End of the current process
|
// End of the current process
|
||||||
@@ -1131,4 +1125,3 @@ void AlbumCoverManager::SaveEmbeddedCoverFinished(TagReaderReplyPtr reply, Album
|
|||||||
LoadAlbumCoverAsync(album_item);
|
LoadAlbumCoverAsync(album_item);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class AlbumCoverSearcher : public QDialog {
|
|||||||
AlbumCoverImageResult Exec(const QString &artist, const QString &album);
|
AlbumCoverImageResult Exec(const QString &artist, const QString &album);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void keyPressEvent(QKeyEvent*) override;
|
void keyPressEvent(QKeyEvent *e) override;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void Search();
|
void Search();
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ CoverSearchStatisticsDialog::CoverSearchStatisticsDialog(QWidget *parent)
|
|||||||
details_layout_ = new QVBoxLayout(ui_->details);
|
details_layout_ = new QVBoxLayout(ui_->details);
|
||||||
details_layout_->setSpacing(0);
|
details_layout_->setSpacing(0);
|
||||||
|
|
||||||
setStyleSheet(
|
setStyleSheet(u"#details {"
|
||||||
u"#details {"
|
|
||||||
" background-color: palette(base);"
|
" background-color: palette(base);"
|
||||||
"}"
|
"}"
|
||||||
"#details QLabel[type=\"label\"] {"
|
"#details QLabel[type=\"label\"] {"
|
||||||
@@ -67,10 +66,7 @@ void CoverSearchStatisticsDialog::Show(const CoverSearchStatistics &statistics)
|
|||||||
QStringList providers(statistics.total_images_by_provider_.keys());
|
QStringList providers(statistics.total_images_by_provider_.keys());
|
||||||
std::sort(providers.begin(), providers.end());
|
std::sort(providers.begin(), providers.end());
|
||||||
|
|
||||||
ui_->summary->setText(tr("Got %1 covers out of %2 (%3 failed)")
|
ui_->summary->setText(tr("Got %1 covers out of %2 (%3 failed)").arg(statistics.chosen_images_).arg(statistics.chosen_images_ + statistics.missing_images_).arg(statistics.missing_images_));
|
||||||
.arg(statistics.chosen_images_)
|
|
||||||
.arg(statistics.chosen_images_ + statistics.missing_images_)
|
|
||||||
.arg(statistics.missing_images_));
|
|
||||||
|
|
||||||
for (const QString &provider : std::as_const(providers)) {
|
for (const QString &provider : std::as_const(providers)) {
|
||||||
AddLine(tr("Covers from %1").arg(provider), QString::number(statistics.chosen_images_by_provider_[provider]));
|
AddLine(tr("Covers from %1").arg(provider), QString::number(statistics.chosen_images_by_provider_[provider]));
|
||||||
|
|||||||
@@ -138,4 +138,3 @@ void CDDADevice::SongLoadingFinished() {
|
|||||||
WatchForDiscChanges(true);
|
WatchForDiscChanges(true);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,3 +159,11 @@ bool CDDALister::Init() {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CDDALister::AskForScan(const QString &id) const {
|
||||||
|
|
||||||
|
Q_UNUSED(id)
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ class CDDALister : public DeviceLister {
|
|||||||
quint64 DeviceCapacity(const QString &id) override;
|
quint64 DeviceCapacity(const QString &id) override;
|
||||||
quint64 DeviceFreeSpace(const QString &id) override;
|
quint64 DeviceFreeSpace(const QString &id) override;
|
||||||
QVariantMap DeviceHardwareInfo(const QString &id) override;
|
QVariantMap DeviceHardwareInfo(const QString &id) override;
|
||||||
bool AskForScan(const QString&) const override { return false; }
|
bool AskForScan(const QString &id) const override;
|
||||||
QString MakeFriendlyName(const QString&) override;
|
QString MakeFriendlyName(const QString &id) override;
|
||||||
QList<QUrl> MakeDeviceUrls(const QString&) override;
|
QList<QUrl> MakeDeviceUrls(const QString &id) override;
|
||||||
void UnmountDevice(const QString&) override;
|
void UnmountDevice(const QString &id) override;
|
||||||
void UpdateDeviceFreeSpace(const QString&) override;
|
void UpdateDeviceFreeSpace(const QString &id) override;
|
||||||
bool Init() override;
|
bool Init() override;
|
||||||
bool CopyMusic() override { return false; }
|
bool CopyMusic() override { return false; }
|
||||||
|
|
||||||
|
|||||||
@@ -227,8 +227,7 @@ void DeviceDatabaseBackend::SetDeviceOptions(const int id, const QString &friend
|
|||||||
QSqlDatabase db(db_->Connect());
|
QSqlDatabase db(db_->Connect());
|
||||||
|
|
||||||
SqlQuery q(db);
|
SqlQuery q(db);
|
||||||
q.prepare(QStringLiteral(
|
q.prepare(QStringLiteral("UPDATE devices"
|
||||||
"UPDATE devices"
|
|
||||||
" SET friendly_name=:friendly_name,"
|
" SET friendly_name=:friendly_name,"
|
||||||
" icon=:icon_name,"
|
" icon=:icon_name,"
|
||||||
" transcode_mode=:transcode_mode,"
|
" transcode_mode=:transcode_mode,"
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ class DeviceDatabaseBackend : public QObject {
|
|||||||
private:
|
private:
|
||||||
SharedPtr<Database> db_;
|
SharedPtr<Database> db_;
|
||||||
QThread *original_thread_;
|
QThread *original_thread_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // DEVICEDATABASEBACKEND_H
|
#endif // DEVICEDATABASEBACKEND_H
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ class DeviceItemDelegate : public CollectionItemDelegate {
|
|||||||
static const int kIconPadding;
|
static const int kIconPadding;
|
||||||
|
|
||||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const override;
|
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &idx) const override;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class DeviceView : public AutoExpandingTreeView {
|
class DeviceView : public AutoExpandingTreeView {
|
||||||
@@ -80,7 +79,7 @@ class DeviceView : public AutoExpandingTreeView {
|
|||||||
bool CanRecursivelyExpand(const QModelIndex &idx) const override;
|
bool CanRecursivelyExpand(const QModelIndex &idx) const override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void contextMenuEvent(QContextMenuEvent*) override;
|
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||||
void mouseDoubleClickEvent(QMouseEvent *e) override;
|
void mouseDoubleClickEvent(QMouseEvent *e) override;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ FilesystemDevice::FilesystemDevice(const QUrl &url,
|
|||||||
QObject::connect(collection_watcher_, &CollectionWatcher::SongsReadded, &*collection_backend_, &CollectionBackend::MarkSongsUnavailable);
|
QObject::connect(collection_watcher_, &CollectionWatcher::SongsReadded, &*collection_backend_, &CollectionBackend::MarkSongsUnavailable);
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsDiscovered, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsDiscovered, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsMTimeUpdated, &*collection_backend_, &CollectionBackend::AddOrUpdateSubdirs);
|
||||||
|
QObject::connect(collection_watcher_, &CollectionWatcher::SubdirsDeleted, &*collection_backend_, &CollectionBackend::DeleteSubdirs);
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*collection_backend_, &CollectionBackend::CompilationsNeedUpdating);
|
QObject::connect(collection_watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*collection_backend_, &CollectionBackend::CompilationsNeedUpdating);
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::UpdateLastSeen, &*collection_backend_, &CollectionBackend::UpdateLastSeen);
|
QObject::connect(collection_watcher_, &CollectionWatcher::UpdateLastSeen, &*collection_backend_, &CollectionBackend::UpdateLastSeen);
|
||||||
QObject::connect(collection_watcher_, &CollectionWatcher::ScanStarted, this, &FilesystemDevice::TaskStarted);
|
QObject::connect(collection_watcher_, &CollectionWatcher::ScanStarted, this, &FilesystemDevice::TaskStarted);
|
||||||
|
|||||||
@@ -160,7 +160,10 @@ QVariantList GioLister::DeviceIcons(const QString &id) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString GioLister::DeviceManufacturer(const QString &id) { Q_UNUSED(id); return QString(); }
|
QString GioLister::DeviceManufacturer(const QString &id) {
|
||||||
|
Q_UNUSED(id);
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
QString GioLister::DeviceModel(const QString &id) {
|
QString GioLister::DeviceModel(const QString &id) {
|
||||||
|
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ class GioLister : public DeviceLister {
|
|||||||
static void VolumeAddedCallback(GVolumeMonitor *volume_monitor, GVolume *volume, gpointer instance);
|
static void VolumeAddedCallback(GVolumeMonitor *volume_monitor, GVolume *volume, gpointer instance);
|
||||||
static void VolumeRemovedCallback(GVolumeMonitor *volume_monitor, GVolume *volume, gpointer instance);
|
static void VolumeRemovedCallback(GVolumeMonitor *volume_monitor, GVolume *volume, gpointer instance);
|
||||||
|
|
||||||
static void MountAddedCallback(GVolumeMonitor *volume_monitor, GMount*, gpointer instance);
|
static void MountAddedCallback(GVolumeMonitor *volume_monitor, GMount *mount, gpointer instance);
|
||||||
static void MountChangedCallback(GVolumeMonitor *volume_monitor, GMount*, gpointer instance);
|
static void MountChangedCallback(GVolumeMonitor *volume_monitor, GMount *mount, gpointer instance);
|
||||||
static void MountRemovedCallback(GVolumeMonitor *volume_monitor, GMount *mount, gpointer instance);
|
static void MountRemovedCallback(GVolumeMonitor *volume_monitor, GMount *mount, gpointer instance);
|
||||||
|
|
||||||
static void VolumeMountFinished(GObject *object, GAsyncResult *result, gpointer instance);
|
static void VolumeMountFinished(GObject *object, GAsyncResult *result, gpointer instance);
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ class MtpLoader : public QObject {
|
|||||||
ScopedPtr<MtpConnection> connection_;
|
ScopedPtr<MtpConnection> connection_;
|
||||||
QThread *original_thread_;
|
QThread *original_thread_;
|
||||||
bool abort_;
|
bool abort_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // MTPLOADER_H
|
#endif // MTPLOADER_H
|
||||||
|
|||||||
@@ -411,10 +411,7 @@ Udisks2Lister::PartitionData Udisks2Lister::ReadPartitionData(const QDBusObjectP
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString Udisks2Lister::PartitionData::unique_id() const {
|
QString Udisks2Lister::PartitionData::unique_id() const {
|
||||||
return u"Udisks2/%1/%2/%3/%4/%5"_s
|
return u"Udisks2/%1/%2/%3/%4/%5"_s.arg(serial, vendor, model).arg(capacity).arg(uuid);
|
||||||
.arg(serial, vendor, model)
|
|
||||||
.arg(capacity)
|
|
||||||
.arg(uuid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Udisks2Lister::Udisks2Job::Udisks2Job() : is_mount(true) {}
|
Udisks2Lister::Udisks2Job::Udisks2Job() : is_mount(true) {}
|
||||||
|
|||||||
@@ -127,8 +127,9 @@ QString About::MainHtml() const {
|
|||||||
ret += "<p>"_L1;
|
ret += "<p>"_L1;
|
||||||
ret += tr("If you like Strawberry and can make use of it, consider sponsoring or donating.");
|
ret += tr("If you like Strawberry and can make use of it, consider sponsoring or donating.");
|
||||||
ret += "<br />"_L1;
|
ret += "<br />"_L1;
|
||||||
ret += tr("You can sponsor the author on %1. You can also make a one-time payment through %2.").arg(
|
ret += tr("You can sponsor the author on %1 or %2. You can also make a one-time payment through %3.").arg(
|
||||||
QStringLiteral("<a style=\"color:%1;\" href=\"https://github.com/sponsors/jonaski\">GitHub sponsors</a>").arg(palette().text().color().name()),
|
QStringLiteral("<a style=\"color:%1;\" href=\"https://www.patreon.com/jonaskvinge\">Patreon</a>").arg(palette().text().color().name()),
|
||||||
|
QStringLiteral("<a style=\"color:%1;\" href=\"https://github.com/sponsors/jonaski\">GitHub</a>").arg(palette().text().color().name()),
|
||||||
QStringLiteral("<a style=\"color:%1;\" href=\"https://paypal.me/jonaskvinge\">paypal.me/jonaskvinge</a>").arg(palette().text().color().name())
|
QStringLiteral("<a style=\"color:%1;\" href=\"https://paypal.me/jonaskvinge\">paypal.me/jonaskvinge</a>").arg(palette().text().color().name())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
|
|
||||||
|
#include "constants/timeconstants.h"
|
||||||
#include "core/iconloader.h"
|
#include "core/iconloader.h"
|
||||||
#include "core/logging.h"
|
#include "core/logging.h"
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
@@ -80,6 +81,7 @@
|
|||||||
#include "utilities/coverutils.h"
|
#include "utilities/coverutils.h"
|
||||||
#include "utilities/coveroptions.h"
|
#include "utilities/coveroptions.h"
|
||||||
#include "tagreader/tagreaderclient.h"
|
#include "tagreader/tagreaderclient.h"
|
||||||
|
#include "tagreader/tagid3v2version.h"
|
||||||
#include "widgets/busyindicator.h"
|
#include "widgets/busyindicator.h"
|
||||||
#include "widgets/lineedit.h"
|
#include "widgets/lineedit.h"
|
||||||
#include "collection/collectionbackend.h"
|
#include "collection/collectionbackend.h"
|
||||||
@@ -103,14 +105,29 @@
|
|||||||
using std::make_shared;
|
using std::make_shared;
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
|
#ifdef __clang__
|
||||||
|
# pragma clang diagnostic push
|
||||||
|
# pragma clang diagnostic ignored "-Wunused-const-variable"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char kSettingsGroup[] = "EditTagDialog";
|
constexpr char kSettingsGroup[] = "EditTagDialog";
|
||||||
constexpr int kSmallImageSize = 128;
|
constexpr int kSmallImageSize = 128;
|
||||||
|
|
||||||
|
// ID3v2 version constants
|
||||||
|
constexpr int kID3v2_Version_3 = 3;
|
||||||
|
constexpr int kID3v2_Version_4 = 4;
|
||||||
|
constexpr int kComboBoxIndex_ID3v2_3 = 0;
|
||||||
|
constexpr int kComboBoxIndex_ID3v2_4 = 1;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
const char EditTagDialog::kTagsDifferentHintText[] = QT_TR_NOOP("(different across multiple songs)");
|
const char EditTagDialog::kTagsDifferentHintText[] = QT_TR_NOOP("(different across multiple songs)");
|
||||||
const char EditTagDialog::kArtDifferentHintText[] = QT_TR_NOOP("Different art across multiple songs.");
|
const char EditTagDialog::kArtDifferentHintText[] = QT_TR_NOOP("Different art across multiple songs.");
|
||||||
|
|
||||||
|
#ifdef __clang_
|
||||||
|
# pragma clang diagnostic pop
|
||||||
|
#endif
|
||||||
|
|
||||||
EditTagDialog::EditTagDialog(const SharedPtr<NetworkAccessManager> network,
|
EditTagDialog::EditTagDialog(const SharedPtr<NetworkAccessManager> network,
|
||||||
const SharedPtr<TagReaderClient> tagreader_client,
|
const SharedPtr<TagReaderClient> tagreader_client,
|
||||||
const SharedPtr<CollectionBackend> collection_backend,
|
const SharedPtr<CollectionBackend> collection_backend,
|
||||||
@@ -394,6 +411,17 @@ bool EditTagDialog::eventFilter(QObject *o, QEvent *e) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SongList EditTagDialog::songs() const {
|
||||||
|
|
||||||
|
SongList result;
|
||||||
|
for (const Data &d : data_) {
|
||||||
|
result << d.current_;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
bool EditTagDialog::SetLoading(const QString &message) {
|
bool EditTagDialog::SetLoading(const QString &message) {
|
||||||
|
|
||||||
const bool loading = !message.isEmpty();
|
const bool loading = !message.isEmpty();
|
||||||
@@ -707,6 +735,9 @@ void EditTagDialog::SelectionChanged() {
|
|||||||
bool titlesort_enabled = false;
|
bool titlesort_enabled = false;
|
||||||
bool artistsort_enabled = false;
|
bool artistsort_enabled = false;
|
||||||
bool albumsort_enabled = false;
|
bool albumsort_enabled = false;
|
||||||
|
bool has_id3v2_support = false;
|
||||||
|
int id3v2_version = 0;
|
||||||
|
bool id3v2_version_different = false;
|
||||||
for (const QModelIndex &idx : indexes) {
|
for (const QModelIndex &idx : indexes) {
|
||||||
if (data_.value(idx.row()).cover_action_ == UpdateCoverAction::None) {
|
if (data_.value(idx.row()).cover_action_ == UpdateCoverAction::None) {
|
||||||
data_[idx.row()].cover_result_ = AlbumCoverImageResult();
|
data_[idx.row()].cover_result_ = AlbumCoverImageResult();
|
||||||
@@ -768,6 +799,15 @@ void EditTagDialog::SelectionChanged() {
|
|||||||
if (song.albumsort_supported()) {
|
if (song.albumsort_supported()) {
|
||||||
albumsort_enabled = true;
|
albumsort_enabled = true;
|
||||||
}
|
}
|
||||||
|
if (song.id3v2_tags_supported()) {
|
||||||
|
has_id3v2_support = true;
|
||||||
|
if (id3v2_version == 0) {
|
||||||
|
id3v2_version = song.id3v2_version();
|
||||||
|
}
|
||||||
|
else if (id3v2_version != song.id3v2_version()) {
|
||||||
|
id3v2_version_different = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString summary;
|
QString summary;
|
||||||
@@ -839,6 +879,23 @@ void EditTagDialog::SelectionChanged() {
|
|||||||
ui_->artistsort->setEnabled(artistsort_enabled);
|
ui_->artistsort->setEnabled(artistsort_enabled);
|
||||||
ui_->albumsort->setEnabled(albumsort_enabled);
|
ui_->albumsort->setEnabled(albumsort_enabled);
|
||||||
|
|
||||||
|
ui_->label_id3v2_version->setVisible(has_id3v2_support);
|
||||||
|
ui_->combobox_id3v2_version->setVisible(has_id3v2_support);
|
||||||
|
|
||||||
|
if (has_id3v2_support) {
|
||||||
|
// Set default based on existing version(s)
|
||||||
|
if (id3v2_version_different || id3v2_version == 0) {
|
||||||
|
// Mixed versions or unknown - default to ID3v2.4
|
||||||
|
ui_->combobox_id3v2_version->setCurrentIndex(kComboBoxIndex_ID3v2_4);
|
||||||
|
}
|
||||||
|
else if (id3v2_version == kID3v2_Version_3) {
|
||||||
|
ui_->combobox_id3v2_version->setCurrentIndex(kComboBoxIndex_ID3v2_3);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ui_->combobox_id3v2_version->setCurrentIndex(kComboBoxIndex_ID3v2_4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EditTagDialog::UpdateUI(const QModelIndexList &indexes) {
|
void EditTagDialog::UpdateUI(const QModelIndexList &indexes) {
|
||||||
@@ -1353,6 +1410,12 @@ void EditTagDialog::SaveData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (save_tags || save_playcount || save_rating || save_embedded_cover) {
|
if (save_tags || save_playcount || save_rating || save_embedded_cover) {
|
||||||
|
// For streaming tracks, skip tag writing since there's no local file.
|
||||||
|
// The metadata will be applied directly to the playlist item in MainWindow::EditTagDialogAccepted.
|
||||||
|
if (ref.current_.is_stream()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Not to confuse the collection model.
|
// Not to confuse the collection model.
|
||||||
if (ref.current_.track() <= 0) { ref.current_.set_track(-1); }
|
if (ref.current_.track() <= 0) { ref.current_.set_track(-1); }
|
||||||
if (ref.current_.disc() <= 0) { ref.current_.set_disc(-1); }
|
if (ref.current_.disc() <= 0) { ref.current_.set_disc(-1); }
|
||||||
@@ -1370,6 +1433,13 @@ void EditTagDialog::SaveData() {
|
|||||||
}
|
}
|
||||||
save_tag_cover_data.cover_data = ref.cover_result_.image_data;
|
save_tag_cover_data.cover_data = ref.cover_result_.image_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine ID3v2 version based on user selection
|
||||||
|
TagID3v2Version tag_id3v2_version = TagID3v2Version::Default;
|
||||||
|
if (ref.current_.filetype() == Song::FileType::MPEG || ref.current_.filetype() == Song::FileType::WAV || ref.current_.filetype() == Song::FileType::AIFF) {
|
||||||
|
tag_id3v2_version = ui_->combobox_id3v2_version->currentIndex() == kComboBoxIndex_ID3v2_3 ? TagID3v2Version::V3 : TagID3v2Version::V4;
|
||||||
|
}
|
||||||
|
|
||||||
TagReaderClient::SaveOptions save_tags_options;
|
TagReaderClient::SaveOptions save_tags_options;
|
||||||
if (save_tags) {
|
if (save_tags) {
|
||||||
save_tags_options |= TagReaderClient::SaveOption::Tags;
|
save_tags_options |= TagReaderClient::SaveOption::Tags;
|
||||||
@@ -1383,7 +1453,7 @@ void EditTagDialog::SaveData() {
|
|||||||
if (save_embedded_cover) {
|
if (save_embedded_cover) {
|
||||||
save_tags_options |= TagReaderClient::SaveOption::Cover;
|
save_tags_options |= TagReaderClient::SaveOption::Cover;
|
||||||
}
|
}
|
||||||
TagReaderReplyPtr reply = tagreader_client_->WriteFileAsync(ref.current_.url().toLocalFile(), ref.current_, save_tags_options, save_tag_cover_data);
|
TagReaderReplyPtr reply = tagreader_client_->WriteFileAsync(ref.current_.url().toLocalFile(), ref.current_, save_tags_options, save_tag_cover_data, tag_id3v2_version);
|
||||||
SharedPtr<QMetaObject::Connection> connection = make_shared<QMetaObject::Connection>();
|
SharedPtr<QMetaObject::Connection> connection = make_shared<QMetaObject::Connection>();
|
||||||
*connection = QObject::connect(&*reply, &TagReaderReply::Finished, this, [this, reply, ref, connection]() {
|
*connection = QObject::connect(&*reply, &TagReaderReply::Finished, this, [this, reply, ref, connection]() {
|
||||||
SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_);
|
SongSaveTagsComplete(reply, ref.current_.url().toLocalFile(), ref.current_, ref.cover_action_);
|
||||||
@@ -1520,7 +1590,7 @@ void EditTagDialog::FetchLyrics() {
|
|||||||
const Song song = data_.value(ui_->song_list->selectionModel()->selectedIndexes().first().row()).current_;
|
const Song song = data_.value(ui_->song_list->selectionModel()->selectedIndexes().first().row()).current_;
|
||||||
lyrics_fetcher_->Clear();
|
lyrics_fetcher_->Clear();
|
||||||
ui_->lyrics->setPlainText(tr("loading..."));
|
ui_->lyrics->setPlainText(tr("loading..."));
|
||||||
lyrics_id_ = static_cast<qint64>(lyrics_fetcher_->Search(song.effective_albumartist(), song.artist(), song.album(), song.title()));
|
lyrics_id_ = static_cast<qint64>(lyrics_fetcher_->Search(song.effective_albumartist(), song.artist(), song.album(), song.title(), song.length_nanosec() / kNsecPerSec));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class EditTagDialog : public QDialog {
|
|||||||
void SetSongs(const SongList &songs, const PlaylistItemPtrList &items = PlaylistItemPtrList());
|
void SetSongs(const SongList &songs, const PlaylistItemPtrList &items = PlaylistItemPtrList());
|
||||||
|
|
||||||
PlaylistItemPtrList playlist_items() const { return playlist_items_; }
|
PlaylistItemPtrList playlist_items() const { return playlist_items_; }
|
||||||
|
SongList songs() const;
|
||||||
void accept() override;
|
void accept() override;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
|
|||||||
@@ -650,6 +650,47 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="layout_id3v2_version">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_id3v2_version">
|
||||||
|
<property name="text">
|
||||||
|
<string>ID3v2 version:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="combobox_id3v2_version">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>2.3</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>2.4</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="spacer_id3v2_version">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="spacer_albumart_bottom">
|
<spacer name="spacer_albumart_bottom">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <QApplication>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
@@ -40,9 +41,8 @@
|
|||||||
|
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
ErrorDialog::ErrorDialog(QWidget *mainwindow, QWidget *parent)
|
ErrorDialog::ErrorDialog(QWidget *parent)
|
||||||
: QDialog(parent),
|
: QDialog(parent),
|
||||||
mainwindow_(mainwindow),
|
|
||||||
ui_(new Ui_ErrorDialog) {
|
ui_(new Ui_ErrorDialog) {
|
||||||
|
|
||||||
ui_->setupUi(this);
|
ui_->setupUi(this);
|
||||||
@@ -64,8 +64,9 @@ ErrorDialog::~ErrorDialog() {
|
|||||||
|
|
||||||
void ErrorDialog::ShowDialog() {
|
void ErrorDialog::ShowDialog() {
|
||||||
|
|
||||||
if (screen() && mainwindow_->screen() && screen() != mainwindow_->screen()) {
|
QWidget *active_window = QApplication::activeWindow();
|
||||||
Utilities::CenterWidgetOnScreen(mainwindow_->screen(), this);
|
if (active_window && screen() && active_window->screen() && screen() != active_window->screen()) {
|
||||||
|
Utilities::CenterWidgetOnScreen(active_window->screen(), this);
|
||||||
}
|
}
|
||||||
setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive);
|
setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive);
|
||||||
show();
|
show();
|
||||||
@@ -81,7 +82,7 @@ void ErrorDialog::ShowMessage(const QString &message) {
|
|||||||
current_messages_ << message;
|
current_messages_ << message;
|
||||||
UpdateContent();
|
UpdateContent();
|
||||||
|
|
||||||
if (mainwindow_->isActiveWindow()) {
|
if (QApplication::activeWindow()) {
|
||||||
ShowDialog();
|
ShowDialog();
|
||||||
}
|
}
|
||||||
else if (!isVisible()) {
|
else if (!isVisible()) {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ErrorDialog : public QDialog {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ErrorDialog(QWidget *mainwindow, QWidget *parent = nullptr);
|
explicit ErrorDialog(QWidget *parent = nullptr);
|
||||||
~ErrorDialog() override;
|
~ErrorDialog() override;
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
@@ -50,7 +50,6 @@ class ErrorDialog : public QDialog {
|
|||||||
private:
|
private:
|
||||||
void UpdateContent();
|
void UpdateContent();
|
||||||
|
|
||||||
QWidget *mainwindow_;
|
|
||||||
Ui_ErrorDialog *ui_;
|
Ui_ErrorDialog *ui_;
|
||||||
|
|
||||||
QStringList current_messages_;
|
QStringList current_messages_;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user