Compare commits

..

106 Commits
1.2.2 ... 1.2.5

Author SHA1 Message Date
Jonas Kvinge
6904efef47 Release 1.2.5 2025-01-17 01:40:38 +01:00
Jonas Kvinge
fafa89baff CI: Disable GIO on Windows 2025-01-16 07:18:31 +01:00
Strawberry Bot
d9062446f5 New translations 2025-01-16 06:39:23 +01:00
Jonas Kvinge
9256b92d8f Update Changelog 2025-01-16 06:32:35 +01:00
Jonas Kvinge
f66459f3cb Make optional feature required unless disabled, add QtSparkle for macOS 2025-01-16 06:22:13 +01:00
Jonas Kvinge
f8ea9631ca Add sparkle 2025-01-15 23:03:40 +01:00
Jonas Kvinge
ab558f87b5 GstEnginePipeline: Use SetStateAsync in finish if needed 2025-01-15 07:01:43 +01:00
Jonas Kvinge
ab73eda2be Add mutex_protected_test 2025-01-15 07:00:28 +01:00
Jonas Kvinge
92f34ff36e mutex_protected: Add more operators 2025-01-15 07:00:16 +01:00
Strawberry Bot
d0bf2d7a9c New translations 2025-01-14 07:10:07 +01:00
Jonas Kvinge
b2cd3afe55 TagReaderClient: Add [[nodiscard]] 2025-01-14 06:36:15 +01:00
Jonas Kvinge
decd0a1dc6 TagReaderClient: Connect TagReaderReplyPtr
Makes sure TagReaderReplyPtr is not deleted too early.

Partial fix for #1633
2025-01-14 06:35:51 +01:00
Jonas Kvinge
3e0a9fa388 SettingsProvider: Use Settings
Fixes #1649
2025-01-13 12:22:31 +01:00
Jonas Kvinge
e5b6c5959f CMake: Use find_package for qtsparkle 2025-01-12 04:16:17 +01:00
Jonas Kvinge
8a9db5440d rpm: Enable unit tests on Fedora 2025-01-11 01:39:26 +01:00
Jonas Kvinge
28a25c5763 CMake: Fix add_test 2025-01-11 01:37:06 +01:00
dependabot[bot]
ea49fbcbee Bump vmactions/openbsd-vm from 1.1.5 to 1.1.6
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.1.5 to 1.1.6.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.1.5...v1.1.6)

---
updated-dependencies:
- dependency-name: vmactions/openbsd-vm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 00:37:52 +01:00
Jonas Kvinge
d9807b358e GioLister: Fix use of deprecated functions 2025-01-11 00:35:24 +01:00
Jonas Kvinge
3e53d2b237 CI: Add -Wno-maybe-uninitialized for Fedora 2025-01-11 00:35:24 +01:00
Jonas Kvinge
9122881f74 CI: Bump GCC on openSUSE Leap 2025-01-11 00:35:24 +01:00
Jonas Kvinge
9c36d7fd43 rpm: Add BUILD_WERROR 2025-01-11 00:35:24 +01:00
Jonas Kvinge
a4a365cbee CI: Set RPM_BUILD_NCPUS to 4 2025-01-11 00:35:24 +01:00
Jonas Kvinge
00f06b22b8 CMake: Check for gmock 2025-01-11 00:35:24 +01:00
Jonas Kvinge
e2d8838fca rpm: Run unit tests 2025-01-11 00:35:24 +01:00
Jonas Kvinge
9427691f39 CI: Install gtest and gmock for openSUSE, Fedora and Mageia 2025-01-11 00:35:24 +01:00
Jonas Kvinge
bebdcc4e7f CollectionModel: Simply data function 2025-01-10 17:47:30 +01:00
Jonas Kvinge
041f761921 test_utils: Fix Q_ASSERT 2025-01-10 15:35:15 +01:00
Jonas Kvinge
1435ae6dc0 Turn on git revision 2025-01-10 15:08:04 +01:00
Jonas Kvinge
33ae53a90f Release 1.2.4 2025-01-10 02:26:41 +01:00
Strawberry Bot
01c28867b7 New translations 2025-01-10 02:10:44 +01:00
Jonas Kvinge
08fb2ae331 Update Changelog 2025-01-10 01:58:19 +01:00
Jonas Kvinge
99970f9e52 Update strawberry_en_US.ts 2025-01-10 01:58:19 +01:00
Jonas Kvinge
bc206f43b4 TranscodeDialog: Minor adjustments 2025-01-10 01:58:19 +01:00
dependabot[bot]
018448159c Bump vmactions/freebsd-vm from 1.1.7 to 1.1.8
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.1.7 to 1.1.8.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.1.7...v1.1.8)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-10 01:27:57 +01:00
Jonas Kvinge
81fe90bdef Update Changelog 2025-01-10 01:07:32 +01:00
Jonas Kvinge
319558c535 tests: Fixed ignored return value 2025-01-10 00:58:41 +01:00
Jonas Kvinge
9095b0d6b2 Always run MSVC runtime installer 2025-01-08 19:34:27 +01:00
Jonas Kvinge
558eae1ca1 Remove unneeded slots 2025-01-08 04:57:17 +01:00
Jonas Kvinge
2c64f05cea MoodbarProxyStyle: Add const 2025-01-08 04:54:45 +01:00
Jonas Kvinge
c80e7071a1 StandardPaths: Fix namespace 2025-01-07 22:13:24 +01:00
Jonas Kvinge
038e679000 SmartPlaylistWizard: Apply dark mode workaround for all styles except vista
Fixes #1639
2025-01-07 22:04:28 +01:00
Jonas Kvinge
72447fecfb StandardPaths: Remove inheritance 2025-01-07 21:40:06 +01:00
Jonas Kvinge
c7830f6f05 MoodbarPipeline: Cleanup on finish 2025-01-07 01:09:49 +01:00
Jonas Kvinge
af525e42b6 MoodbarPipeline: Remove Q_ASSERT 2025-01-06 21:28:44 +01:00
Jonas Kvinge
eb83f23125 MoodbarPipeline: Use bytearray directly 2025-01-06 21:28:33 +01:00
Jonas Kvinge
7527d2ea9a MoodbarLoader: Set object name 2025-01-06 18:11:38 +01:00
Jonas Kvinge
69d38879d2 Use QCoreApplication::applicationName() directly 2025-01-06 00:38:51 +01:00
Jonas Kvinge
cbce9f7191 Override config, data and cache location 2025-01-05 23:45:29 +01:00
Jonas Kvinge
f938129d81 CI: Fix deb copy command 2025-01-05 21:22:08 +01:00
Jonas Kvinge
415a40ea04 FilterParser: Ignore space after operator 2025-01-05 20:25:48 +01:00
Jonas Kvinge
6e7aaed4ee Use QSharedPointer for GstEnginePipeline 2025-01-05 19:03:16 +01:00
Jonas Kvinge
7afae70bb0 GstEnginePipeline: Make sure all set states are finished before finishing pipeline 2025-01-05 18:58:03 +01:00
Jonas Kvinge
be8097919b mutex_protected: Add operator ++ and -- 2025-01-05 18:56:22 +01:00
Jonas Kvinge
1990a42e1d CI: Don't run build on l10n_master branch 2025-01-05 18:53:19 +01:00
Kyle Hopkins
8fcee4511d TranscodeDialog: update to optionally preserve directory structure 2025-01-05 18:34:14 +01:00
Leandro Matheus
24af1be666 Subsonic: Add support for using album id to retrieve album covers 2025-01-05 18:30:24 +01:00
Jonas Kvinge
8302a95bc1 Use shared pointers for moodbar pipelines
Possible fix for #1633
2025-01-05 18:28:41 +01:00
Jonas Kvinge
36a8ab49a0 MoodbarItemDelegate: Format comment 2025-01-05 03:51:00 +01:00
Jonas Kvinge
5c64dc9c4d MoodbarItemDelegate: Delete data if it fails to insert to cache 2025-01-05 03:50:51 +01:00
Strawberry Bot
c271743208 New translations 2025-01-05 00:22:50 +01:00
Jonas Kvinge
ee49b1ddc8 Update strawberry_en_US.ts 2025-01-04 04:56:08 +01:00
Jonas Kvinge
bf98633f16 Load XSPF title as playlist name 2025-01-04 04:52:17 +01:00
Jonas Kvinge
e2a928f2dc Save XSPF playlist with title
Fixes #1624
2025-01-04 03:48:53 +01:00
Jonas Kvinge
47d3312a6b PlaylistManager: Remove slash from playlist filename 2025-01-04 03:46:03 +01:00
Jonas Kvinge
4f97325953 CollectionSettingsPage: Fix string conversion 2025-01-04 03:32:50 +01:00
Jonas Kvinge
91e8fe0943 ScrobblerSettingsPage: Add tooltip 2025-01-04 03:15:38 +01:00
Jonas Kvinge
dc5894b38a CollectionWatcher: Ignore special filesystem paths
Fixes #1615
2025-01-04 03:06:46 +01:00
Jonas Kvinge
52ee50a2a4 CollectionSettingsPage: Add check for filesystem type 2025-01-04 02:58:59 +01:00
Jonas Kvinge
f545b028ee Add filesystem constants 2025-01-04 02:58:17 +01:00
Jonas Kvinge
a96627c5a9 Playlist: Invalidate deleted songs in main thread
Fixes #1625
2025-01-04 00:16:36 +01:00
Jonas Kvinge
a13fc31f83 PlaylistManager: Remove unused InvalidateDeletedSongs function 2025-01-04 00:09:26 +01:00
Jonas Kvinge
9ee5c8dc17 Playlist: Update copyright 2025-01-03 23:41:43 +01:00
Jonas Kvinge
82cd425ece Playlist: Add const 2025-01-03 23:41:30 +01:00
Jonas Kvinge
3b02d364ba Playlist: Rename some variables 2025-01-03 23:41:15 +01:00
dependabot[bot]
73e7947487 Bump vmactions/openbsd-vm from 1.1.4 to 1.1.5
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.1.4 to 1.1.5.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.1.4...v1.1.5)

---
updated-dependencies:
- dependency-name: vmactions/openbsd-vm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 17:38:02 +01:00
dependabot[bot]
cfc9a43b88 Bump vmactions/freebsd-vm from 1.1.6 to 1.1.7
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.1.6 to 1.1.7.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.1.6...v1.1.7)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 17:37:32 +01:00
dependabot[bot]
969500023a Bump vmactions/freebsd-vm from 1.1.5 to 1.1.6
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.1.5 to 1.1.6.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.1.5...v1.1.6)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-26 00:44:32 +01:00
Jonas Kvinge
53c72d4f8e CollectionModel: Don't use artist sort text for album 2024-12-17 22:38:04 +01:00
Jonas Kvinge
f971c92f32 Move debian back to source dir
PPA failed to build
2024-12-17 21:57:37 +01:00
Strawberry Bot
82156e8a13 New translations 2024-12-17 00:01:04 +01:00
dependabot[bot]
6a6285861e Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 23:59:25 +01:00
Jonas Kvinge
dae8c8730b debian: Remove whitespaces 2024-12-14 18:33:40 +01:00
Jonas Kvinge
20b47eae8f CI: Use -j 4 for Ubuntu 2024-12-14 18:33:29 +01:00
Jonas Kvinge
2ce8220d88 Move generated files to binary directory 2024-12-14 17:59:26 +01:00
Jonas Kvinge
35b0b5df57 CMake: Fix backtrace linking 2024-12-14 15:47:45 +01:00
Jonas Kvinge
63631d6b0c CMake: Fix backtrace linking 2024-12-14 05:30:31 +01:00
Jonas Kvinge
21ab2ef1a7 CI: Add FreeBSD and OpenBSD 2024-12-14 05:01:40 +01:00
Jonas Kvinge
a3f96d2b85 CMake: Link Backtrace 2024-12-14 05:01:14 +01:00
Jonas Kvinge
e2c1cb0116 Formatting 2024-12-14 00:55:53 +01:00
Jonas Kvinge
07e295776b Add Spotify to scrobbler 2024-12-10 01:29:57 +01:00
Jonas Kvinge
8604a39d94 Turn on git revision 2024-12-08 17:29:14 +01:00
Jonas Kvinge
315240fec7 Release 1.2.3 2024-12-08 16:21:28 +01:00
Jonas Kvinge
fbc6d326f8 Update Changelog 2024-12-08 16:15:24 +01:00
Strawberry Bot
c082377e7a New translations 2024-12-07 19:21:29 +01:00
Jonas Kvinge
448445a38a CI: Replace redhat-lsb-core with lsb_release 2024-12-07 14:42:15 +01:00
Jonas Kvinge
18f835d7e5 Mpris2: Check for valid current row 2024-12-07 14:36:44 +01:00
Jonas Kvinge
fd427dac29 Handle missing HTTP status code 2024-12-07 14:02:59 +01:00
Jonas Kvinge
e1afe03d51 Check for valid http status code 2024-12-07 00:32:06 +01:00
Jonas Kvinge
1b49653974 Update Changelog 2024-12-06 23:55:25 +01:00
Jonas Kvinge
d66126f998 GstEngine: Add missing seek
Fixes #1568
2024-12-06 23:44:27 +01:00
Jonas Kvinge
0fff5f672a Rename variables 2024-12-06 23:43:44 +01:00
Strawberry Bot
2726f01fb3 New translations 2024-12-04 22:41:27 +01:00
Jonas Kvinge
9a74fce53d DiscogsCoverProvider: Use anonymous namespace for constants 2024-12-02 22:01:00 +01:00
Jonas Kvinge
b3be8387f1 Song: Add .zst to rejected file extensions
Fixes #1612
2024-11-29 23:37:49 +01:00
Jonas Kvinge
d396cb515d Include cstddef before libcdio includes
Fixes #1610
2024-11-29 22:50:49 +01:00
Jonas Kvinge
2548b4648e Turn on git revision 2024-11-23 19:34:13 +01:00
160 changed files with 5312 additions and 5522 deletions

View File

@@ -10,7 +10,7 @@ jobs:
build-opensuse:
name: Build openSUSE
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -21,18 +21,18 @@ jobs:
steps:
- name: Refresh repositories
run: zypper -n --gpg-auto-import-keys ref
- name: Upgrade packages
- name: Upgrade packages (Tumbleweed)
if: matrix.opensuse_version == 'tumbleweed'
run: zypper -n --gpg-auto-import-keys dup
- name: Upgrade packages
- name: Upgrade packages (Leap)
if: matrix.opensuse_version != 'tumbleweed'
run: zypper -n --gpg-auto-import-keys up
- name: Install gcc
- name: Install gcc (Tumbleweed)
if: matrix.opensuse_version == 'tumbleweed'
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
- name: Install gcc 13
- name: Install gcc (Leap)
if: matrix.opensuse_version != 'tumbleweed'
run: zypper -n --gpg-auto-import-keys in gcc13 gcc13-c++
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
- name: Install packages
run: >
zypper -n --gpg-auto-import-keys in
@@ -77,6 +77,8 @@ jobs:
qt6-base-common-devel
qt6-sql-sqlite
qt6-linguist-devel
gtest
gmock
- name: Install kdsingleapplication-qt6-devel
if: matrix.opensuse_version == 'tumbleweed'
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
@@ -101,15 +103,18 @@ jobs:
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
- name: Build RPM (Tumbleweed)
if: matrix.opensuse_version == 'tumbleweed'
env:
RPM_BUILD_NCPUS: 4
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
run: rpmbuild -ba strawberry.spec
- name: Build RPM (Leap)
if: matrix.opensuse_version != 'tumbleweed'
working-directory: build
env:
CC: gcc-13
CXX: g++-13
run: rpmbuild -ba ../dist/unix/strawberry.spec
RPM_BUILD_NCPUS: 4
CC: gcc-14
CXX: g++-14
working-directory: build
run: rpmbuild -ba strawberry.spec
- name: Set subdir
id: set-subdir
run: echo "subdir=$(echo ${{matrix.opensuse_version}} | sed 's/leap:/lp/g' | sed 's/\.//g')" > $GITHUB_OUTPUT
@@ -132,7 +137,7 @@ jobs:
build-fedora:
name: Build Fedora
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -149,7 +154,7 @@ jobs:
run: >
dnf -y install
@development-tools
redhat-lsb-core
lsb_release
which
git
glibc
@@ -185,6 +190,8 @@ jobs:
libappstream-glib
hicolor-icon-theme
kdsingleapplication-qt6-devel
gtest-devel
gmock-devel
- name: Checkout
uses: actions/checkout@v4
with:
@@ -207,9 +214,9 @@ jobs:
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
- name: Build RPM
env:
RPM_BUILD_NCPUS: "2"
RPM_BUILD_NCPUS: 4
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
run: rpmbuild -ba strawberry.spec
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
@@ -221,7 +228,7 @@ jobs:
build-openmandriva:
name: Build OpenMandriva
if: github.repository != 'strawberrymusicplayer/strawberry-private' && false
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master' && false
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -300,9 +307,9 @@ jobs:
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
- name: Build RPM
env:
RPM_BUILD_NCPUS: "2"
RPM_BUILD_NCPUS: 4
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
run: rpmbuild -ba strawberry.spec
- name: Upload artifacts
if: matrix.openmandriva_version != 'cooker'
uses: actions/upload-artifact@v4
@@ -315,7 +322,7 @@ jobs:
build-mageia:
name: Build Mageia
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -374,6 +381,7 @@ jobs:
desktop-file-utils
appstream-util
hicolor-icon-theme
gtest
- name: Checkout
uses: actions/checkout@v4
with:
@@ -396,9 +404,9 @@ jobs:
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
- name: Build RPM
env:
RPM_BUILD_NCPUS: "2"
RPM_BUILD_NCPUS: 4
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
run: rpmbuild -ba strawberry.spec
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
@@ -410,7 +418,7 @@ jobs:
build-debian:
name: Build Debian
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -471,21 +479,24 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_WERROR=ON
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
- name: Delete build directory
run: rm -rf build
- name: make deb
run: dpkg-buildpackage -b -d -uc -us -nc -j2
run: dpkg-buildpackage -b -d -uc -us -nc -j4
- name: Copy deb
run: cp ../*.deb .
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: debian-${{matrix.debian_version}}
path: "*.deb"
path: |
*.deb
build-ubuntu:
name: Build Ubuntu
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -549,9 +560,11 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_WERROR=ON
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
- name: Delete build directory
run: rm -rf build
- name: make deb
run: dpkg-buildpackage -b -d -uc -us -nc -j2
run: dpkg-buildpackage -b -d -uc -us -nc -j4
- name: Copy deb
run: cp ../*.deb ../*.ddeb .
- name: Upload artifacts
@@ -565,7 +578,7 @@ jobs:
upload-ubuntu-ppa:
name: Upload Ubuntu PPA
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && (github.event_name == 'release' || (github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/ci' || github.ref == 'refs/heads/1.1'))) && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && (github.event_name == 'release' || (github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/ci' || github.ref == 'refs/heads/1.1')))
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -634,7 +647,7 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_WERROR=ON
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
- name: Delete build directory
run: rm -rf build
- name: Import Ubuntu PPA GPG private key
@@ -651,9 +664,60 @@ jobs:
run: dput ppa:jonaski/strawberry ../*_source.changes
build-freebsd:
name: Build FreeBSD
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.1.8
with:
usesh: true
mem: 4096
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
run: |
set -e
git config --global --add safe.directory ${GITHUB_WORKSPACE}
cmake -E make_directory build
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug"
cmake --build build --config Debug --parallel 4
build-openbsd:
name: Build OpenBSD
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Build OpenBSD
id: build-openbsd
uses: vmactions/openbsd-vm@v1.1.6
with:
usesh: true
mem: 4096
prepare: pkg_add git cmake pkgconf boost glib2 qt6-qtbase qt6-qttools sqlite gstreamer1 gstreamer1-plugins-base chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf libgpod fftw3 icu4c kdsingleapplication pulseaudio
run: |
set -e
export LDFLAGS="-L/usr/local/lib"
git config --global --add safe.directory ${GITHUB_WORKSPACE}
cmake -E make_directory build
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug" -DENABLE_ALSA=OFF
cmake --build build --config Debug --parallel 4
build-macos-public:
name: Build macOS Public
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
strategy:
fail-fast: false
@@ -737,10 +801,12 @@ jobs:
-DCMAKE_PREFIX_PATH="${{env.prefix_path}}/lib/cmake"
-DBUILD_WERROR=ON
-DUSE_BUNDLE=ON
-DENABLE_DBUS=OFF
-DICU_ROOT="${{env.prefix_path}}"
-DAPPLE_DEVELOPER_ID=$(test '${{github.repository}}' = 'strawberrymusicplayer/strawberry' && test '${{github.event.pull_request.base.repo.full_name}}' = '${{github.event.pull_request.head.repo.full_name}}' && echo "383J84DVB6" || echo "")
-DARCH="${{env.arch}}"
-DENABLE_SPOTIFY=$(test -f "${{env.prefix_path}}/lib/gstreamer-1.0/libgstspotify.dylib" && echo "ON" || echo "OFF")
-DENABLE_SPARKLE=ON
-DENABLE_QTSPARKLE=OFF
- name: Build
run: cmake --build build --config Release --parallel 4
@@ -876,10 +942,12 @@ jobs:
-DCMAKE_PREFIX_PATH="${{env.prefix_path}}/lib/cmake"
-DBUILD_WERROR=ON
-DUSE_BUNDLE=ON
-DENABLE_DBUS=OFF
-DICU_ROOT="${{env.prefix_path}}"
-DAPPLE_DEVELOPER_ID="383J84DVB6"
-DARCH="${{env.arch}}"
-DENABLE_SPOTIFY=$(test -f "${{env.prefix_path}}/lib/gstreamer-1.0/libgstspotify.dylib" && echo "ON" || echo "OFF")
-DENABLE_SPARKLE=ON
-DENABLE_QTSPARKLE=OFF
- name: Build
run: cmake --build build --config Release --parallel 4
@@ -931,7 +999,7 @@ jobs:
build-windows-mingw:
name: Build Windows MinGW
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -974,7 +1042,7 @@ jobs:
-DBUILD_WERROR=ON
-DARCH="${{matrix.arch}}"
-DENABLE_WIN32_CONSOLE=$(test "${{matrix.buildtype}}" = "debug" && echo "ON" || echo "OFF")
-DENABLE_DBUS=OFF
-DENABLE_GIO=OFF
-DENABLE_AUDIOCD=OFF
-DENABLE_MTP=OFF
-DENABLE_GPOD=OFF
@@ -1120,7 +1188,7 @@ jobs:
build-windows-msvc:
name: Build Windows MSVC
if: github.repository != 'strawberrymusicplayer/strawberry-private' && (!(github.event.pusher.name == 'strawbsbot' && contains(github.event.head_commit.message, 'New translations')))
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: windows-2022
strategy:
fail-fast: false
@@ -1262,6 +1330,10 @@ jobs:
-DENABLE_WIN32_CONSOLE=${{env.win32_console}}
-DPKG_CONFIG_EXECUTABLE="${{env.prefix_path_forwardslash}}/bin/pkg-config.exe"
-DICU_ROOT="${{env.prefix_path_forwardslash}}"
-DENABLE_GIO=OFF
-DENABLE_AUDIOCD=OFF
-DENABLE_MTP=OFF
-DENABLE_GPOD=OFF
-DENABLE_SPOTIFY=ON
- name: Run Make
@@ -1396,6 +1468,11 @@ jobs:
exit 1
fi
- name: Download MSVC runtime
shell: bash
working-directory: build
run: curl -f -O -L https://aka.ms/vs/17/release/vc_redist.$(test "${{matrix.arch}}" = "x86_64" && echo "x64" || echo "${{matrix.arch}}").exe
- name: Create nsis installer
shell: cmd
working-directory: build

2
.gitignore vendored
View File

@@ -11,6 +11,4 @@
/out
/CMakeSettings.json
/dist/scripts/maketarball.sh
/dist/unix/strawberry.spec
/debian/changelog
/dist/macos/Info.plist

View File

@@ -18,24 +18,19 @@ include(CheckCXXSourceRuns)
include(CheckIncludeFiles)
include(FindPkgConfig)
include(cmake/Version.cmake)
include(cmake/Summary.cmake)
include(cmake/OptionalComponent.cmake)
include(cmake/OptionalSource.cmake)
include(cmake/ParseArguments.cmake)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LINUX ON)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
set(FREEBSD ON)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "OpenBSD")
set(OPENBSD ON)
endif()
if(LINUX)
include(cmake/Rpm.cmake)
include(cmake/Deb.cmake)
endif()
if(APPLE)
include(cmake/Dmg.cmake)
endif()
@@ -72,7 +67,6 @@ option(USE_BUNDLE "Bundle dependencies" ${USE_BUNDLE_DEFAULT})
if(WIN32)
option(ENABLE_WIN32_CONSOLE "Show the windows console even outside Debug mode" ${ENABLE_WIN32_CONSOLE_DEFAULT})
option(USE_QTSPARKLE "Use Qt Sparkle updater" ON)
endif()
if(MSVC)
@@ -167,17 +161,17 @@ if(NOT Boost_FOUND)
find_package(Boost REQUIRED)
endif()
find_package(ICU COMPONENTS uc i18n REQUIRED)
if(LINUX)
find_package(ALSA REQUIRED)
else()
find_package(ALSA)
endif()
if(UNIX AND NOT APPLE)
if(LINUX)
find_package(ALSA REQUIRED)
else()
find_package(ALSA)
endif()
find_package(X11 COMPONENTS X11_xcb)
endif()
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GOBJECT REQUIRED IMPORTED_TARGET gobject-2.0)
if(UNIX AND NOT APPLE)
if(NOT APPLE)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
if(GIO_FOUND AND UNIX)
pkg_check_modules(GIO_UNIX IMPORTED_TARGET gio-unix-2.0)
@@ -191,7 +185,9 @@ pkg_check_modules(GSTREAMER_APP REQUIRED IMPORTED_TARGET gstreamer-app-1.0)
pkg_check_modules(GSTREAMER_TAG REQUIRED IMPORTED_TARGET gstreamer-tag-1.0)
pkg_check_modules(GSTREAMER_PBUTILS REQUIRED IMPORTED_TARGET gstreamer-pbutils-1.0)
pkg_check_modules(SQLITE REQUIRED IMPORTED_TARGET sqlite3>=3.9)
pkg_check_modules(LIBPULSE IMPORTED_TARGET libpulse)
if(UNIX AND NOT APPLE)
pkg_check_modules(LIBPULSE IMPORTED_TARGET libpulse)
endif()
pkg_check_modules(CHROMAPRINT IMPORTED_TARGET libchromaprint>=1.4)
pkg_check_modules(FFTW3 IMPORTED_TARGET fftw3)
pkg_check_modules(LIBEBUR128 IMPORTED_TARGET libebur128)
@@ -271,33 +267,36 @@ else()
add_definitions(-DKDSINGLEAPPLICATION_STATIC_BUILD)
endif()
# if(APPLE)
# find_package(SPMediaKeyTap REQUIRED)
# endif()
if(APPLE)
find_library(SPARKLE Sparkle)
#find_package(SPMediaKeyTap REQUIRED)
endif()
if(WIN32)
find_package(getopt-win REQUIRED)
if(USE_QTSPARKLE)
pkg_check_modules(QTSPARKLE REQUIRED IMPORTED_TARGET qtsparkle-qt${QT_VERSION_MAJOR})
set(HAVE_QTSPARKLE ON)
endif()
if(APPLE OR WIN32)
find_package(qtsparkle-qt${QT_VERSION_MAJOR})
if(TARGET "qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle")
set(QTSPARKLE_FOUND ON)
endif()
endif()
optional_component(ALSA ON "ALSA integration"
DEPENDS "alsa" ALSA_FOUND
)
optional_component(PULSE ON "PulseAudio integration"
DEPENDS "libpulse" LIBPULSE_FOUND
)
optional_component(DBUS ON "D-Bus support"
DEPENDS "Qt D-Bus" DBUS_FOUND
)
optional_component(MPRIS2 ON "MPRIS2 D-Bus Interface"
DEPENDS "D-Bus support" HAVE_DBUS
)
if(UNIX AND NOT APPLE)
optional_component(ALSA ON "ALSA integration"
DEPENDS "alsa" ALSA_FOUND
)
optional_component(PULSE ON "PulseAudio integration"
DEPENDS "libpulse" LIBPULSE_FOUND
)
optional_component(DBUS ON "D-Bus support"
DEPENDS "Qt D-Bus" DBUS_FOUND
)
optional_component(MPRIS2 ON "MPRIS2 D-Bus Interface"
DEPENDS "D-Bus support" HAVE_DBUS
)
endif()
optional_component(SONGFINGERPRINTING ON "Song fingerprinting and tracking"
DEPENDS "chromaprint" CHROMAPRINT_FOUND
@@ -307,26 +306,29 @@ optional_component(MUSICBRAINZ ON "MusicBrainz integration"
DEPENDS "chromaprint" CHROMAPRINT_FOUND
)
optional_component(X11_GLOBALSHORTCUTS ON "X11 global shortcuts"
DEPENDS "X11" X11_FOUND
DEPENDS "QX11Application" HAVE_QX11APPLICATION
)
if(UNIX AND NOT APPLE)
optional_component(X11_GLOBALSHORTCUTS ON "X11 global shortcuts"
DEPENDS "X11" X11_FOUND
DEPENDS "QX11Application" HAVE_QX11APPLICATION
)
optional_component(KGLOBALACCEL_GLOBALSHORTCUTS ON "KGlobalAccel global shortcuts"
DEPENDS "D-Bus support" HAVE_DBUS
)
optional_component(UDISKS2 ON "Devices: UDisks2 backend"
DEPENDS "D-Bus support" HAVE_DBUS
)
endif()
optional_component(KGLOBALACCEL_GLOBALSHORTCUTS ON "KGlobalAccel global shortcuts"
DEPENDS "D-Bus support" HAVE_DBUS
)
optional_component(UDISKS2 ON "Devices: UDisks2 backend"
DEPENDS "D-Bus support" HAVE_DBUS
)
optional_component(GIO ON "Devices: GIO device backend"
DEPENDS "libgio" GIO_FOUND
)
optional_component(GIO_UNIX ON "Devices: GIO device backend (Unix support)"
DEPENDS "libgio-unix" GIO_UNIX_FOUND
)
if(NOT APPLE)
optional_component(GIO ON "Devices: GIO device backend"
DEPENDS "libgio" GIO_FOUND
)
if(UNIX)
optional_component(GIO_UNIX ON "Devices: GIO device backend (Unix support)"
DEPENDS "libgio-unix" GIO_UNIX_FOUND
)
endif()
endif()
optional_component(AUDIOCD ON "Devices: Audio CD support"
DEPENDS "libcdio" LIBCDIO_FOUND
@@ -358,6 +360,18 @@ optional_component(EBUR128 ON "EBU R 128 loudness normalization"
DEPENDS "libebur128" LIBEBUR128_FOUND
)
if(APPLE)
optional_component(SPARKLE ON "Sparkle integration"
DEPENDS "Sparkle" SPARKLE
)
endif()
if(APPLE OR WIN32)
optional_component(QTSPARKLE ON "QtSparkle integration"
DEPENDS "QtSparkle" QTSPARKLE_FOUND
)
endif()
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
set(HAVE_CHROMAPRINT ON)
endif()
@@ -392,7 +406,7 @@ add_executable(strawberry)
if(APPLE)
set_target_properties(strawberry PROPERTIES MACOSX_BUNDLE TRUE)
set_target_properties(strawberry PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/dist/macos/Info.plist")
set_target_properties(strawberry PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/dist/macos/Info.plist")
endif()
if(WIN32 AND NOT ENABLE_WIN32_CONSOLE)
@@ -443,6 +457,7 @@ set(SOURCES
src/core/enginemetadata.cpp
src/core/songmimedata.cpp
src/core/platforminterface.cpp
src/core/standardpaths.cpp
src/utilities/strutils.cpp
src/utilities/envutils.cpp
@@ -1202,6 +1217,7 @@ if(APPLE)
src/osd/osdmac.h
src/device/macosdevicelister.h
)
optional_source(HAVE_SPARKLE SOURCES src/core/sparkleupdater.mm HEADERS src/core/sparkleupdater.h)
else()
list(APPEND SOURCES src/systemtrayicon/qtsystemtrayicon.cpp src/widgets/searchfield_qt.cpp src/widgets/searchfield_qt_private.cpp)
list(APPEND HEADERS src/systemtrayicon/qtsystemtrayicon.h src/widgets/searchfield_qt_private.h)
@@ -1306,9 +1322,7 @@ optional_source(HAVE_MOODBAR
src/settings/moodbarsettingspage.ui
)
if(UNIX)
optional_source(HAVE_GIO SOURCES src/device/giolister.cpp HEADERS src/device/giolister.h)
endif()
optional_source(HAVE_GIO SOURCES src/device/giolister.cpp HEADERS src/device/giolister.h)
if(HAVE_UDISKS2)
optional_source(HAVE_UDISKS2 SOURCES src/device/udisks2lister.cpp HEADERS src/device/udisks2lister.h)
@@ -1456,7 +1470,7 @@ endif()
add_subdirectory(src)
add_subdirectory(dist)
if(TARGET GTest::GTest AND Qt${QT_VERSION_MAJOR}Test_FOUND)
if(TARGET GTest::gtest AND TARGET GTest::gmock AND Qt${QT_VERSION_MAJOR}Test_FOUND)
add_subdirectory(tests)
endif()
@@ -1491,6 +1505,7 @@ endif()
target_link_libraries(strawberry_lib PUBLIC
${CMAKE_THREAD_LIBS_INIT}
$<$<BOOL:${HAVE_BACKTRACE}>:${Backtrace_LIBRARIES}>
PkgConfig::GLIB
PkgConfig::GOBJECT
PkgConfig::SQLITE
@@ -1521,8 +1536,7 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${HAVE_AUDIOCD}>:PkgConfig::LIBCDIO>
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
$<$<BOOL:${HAVE_QTSPARKLE}>:PkgConfig::QTSPARKLE>
$<$<BOOL:${FREEBSD}>:execinfo>
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
$<$<BOOL:${WIN32}>:dsound dwmapi getopt-win::getopt>
$<$<BOOL:${MSVC}>:WindowsApp>
${SINGLEAPPLICATION_LIBRARIES}
@@ -1538,6 +1552,10 @@ if(APPLE)
"-framework IOKit"
"-framework ScriptingBridge"
)
if(HAVE_SPARKLE)
target_include_directories(strawberry_lib SYSTEM PRIVATE ${SPARKLE}/Headers)
target_link_libraries(strawberry_lib PRIVATE ${SPARKLE})
endif()
endif()
target_link_libraries(strawberry PUBLIC strawberry_lib)
@@ -1553,7 +1571,7 @@ endif()
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" IMMEDIATE @ONLY)
add_custom_target(uninstall "${CMAKE_COMMAND}" -P "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake")
summary_show()
optional_component_summary_show()
if(NOT CMAKE_CROSSCOMPILING AND NOT QT_SQLITE_TEST)
message(WARNING "The Qt sqlite driver test failed.")

View File

@@ -2,6 +2,42 @@ Strawberry Music Player
=======================
ChangeLog
Version 1.2.5 (2025.01.17):
Bugfixes:
* Fixed crash when saving playcount or rating to file (#1633).
* Fixed QFile::open failing in unit tests.
* Fixed playlist sequence settings saved to wrong configuration file (#1649).
Enhancements:
* Fixed use of deprecated GIO functions with GLib 2.84 and newer.
* (macOS) Added back Sparkle updater to check for new releases.
Version 1.2.4 (2025.01.10):
Bugfixes:
* Fixed Spotify songs not being available for scrobbling.
* Fixed leading "A" and "The" articles being skipped for album sort text.
* Fixed thread safety issue when validating playlist songs on startup.
* Fixed filter search not ignoring space after colon when using column based search.
* Fixed KGlobalAccel to use capitalized application name.
* Fixed slash not properly handled when saving a playlist (#1624).
* (Unix) Fixed collection scanner so it ignores special filesystem paths (/sys, /proc, /run, etc) (#1615).
* (Windows) Fixed smart playlist wizard not respecting dark mode with Windows 11 style (#1639).
Enhancements:
* Use XSPF "title" as playlist name when loading and saving playlists (#1624).
* Added support for using album ID when receving album covers for Subsonic songs (#1636).
* Added option for preserving directory structure when trascoding songs (#1637).
* (Windows) Always run MSVC runtime installer to possible fix issues when there is an older runtime installed.
Version 1.2.3 (2024.12.08):
Bugfixes:
* Fixed libcdio NULL related compilation error on FreeBSD (#1610).
* Fixed missing seek when starting playback of a CUE song (#1568).
* Fixed "QDBusObjectPath: invalid path" error.
Version 1.2.2 (2024.11.23):
Bugfixes:

View File

@@ -2,5 +2,5 @@ find_program(LSB_RELEASE_EXEC lsb_release)
find_program(DPKG_BUILDPACKAGE dpkg-buildpackage)
if (LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
add_custom_target(deb WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${DPKG_BUILDPACKAGE} -b -d -uc -us)
add_custom_target(deb WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${DPKG_BUILDPACKAGE} -b -d -uc -us -nc -j4)
endif()

View File

@@ -31,7 +31,7 @@ if(MACDEPLOYQT_EXECUTABLE)
add_custom_target(deploy
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/{Frameworks,Resources}
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
COMMAND cp -v ${CMAKE_BINARY_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}

View File

@@ -1,15 +1,15 @@
set(summary_willbuild "")
set(summary_willnotbuild "")
macro(summary_add name test)
macro(optional_component_summary_add name test)
if (${test})
list(APPEND summary_willbuild ${name})
else (${test})
list(APPEND summary_willnotbuild "${name}")
endif (${test})
endmacro(summary_add)
endmacro(optional_component_summary_add)
macro(summary_show_part variable title)
macro(optional_component_summary_show_part variable title)
list(LENGTH ${variable} _len)
if (_len)
message("")
@@ -18,19 +18,20 @@ macro(summary_show_part variable title)
message(" ${_item}")
endforeach (_item)
endif (_len)
endmacro(summary_show_part)
endmacro(optional_component_summary_show_part)
macro(summary_show)
macro(optional_component_summary_show)
list(SORT summary_willbuild)
list(SORT summary_willnotbuild)
message("")
message("Building strawberry version: ${STRAWBERRY_VERSION_DISPLAY}, Qt version ${Qt${QT_VERSION_MAJOR}Core_VERSION}")
summary_show_part(summary_willbuild "The following components will be built:")
summary_show_part(summary_willnotbuild "The following components WILL NOT be built:")
optional_component_summary_show_part(summary_willbuild "The following components will be built:")
optional_component_summary_show_part(summary_willnotbuild "The following components WILL NOT be built:")
message("")
endmacro(summary_show)
endmacro(optional_component_summary_show)
function(optional_component name default description)
set(option_variable "ENABLE_${name}")
set(have_variable "HAVE_${name}")
set(${have_variable} OFF)
@@ -79,6 +80,9 @@ function(optional_component name default description)
set(text "${description} (missing ${deplist_text})")
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
else()
set(${have_variable} ON PARENT_SCOPE)
set(summary_willbuild "${summary_willbuild};${description}" PARENT_SCOPE)

View File

@@ -64,7 +64,7 @@ if (LSB_RELEASE_EXEC AND RPMBUILD_EXEC)
add_custom_target(rpm
COMMAND ${CMAKE_SOURCE_DIR}/dist/scripts/maketarball.sh
COMMAND ${CMAKE_COMMAND} -E copy strawberry-${STRAWBERRY_VERSION_PACKAGE}.tar.xz ${RPMBUILD_DIR}/SOURCES/
COMMAND ${RPMBUILD_EXEC} -ba ${CMAKE_SOURCE_DIR}/dist/unix/strawberry.spec
COMMAND ${RPMBUILD_EXEC} -ba ${CMAKE_BINARY_DIR}/strawberry.spec
)
endif()

View File

@@ -1,6 +1,6 @@
set(STRAWBERRY_VERSION_MAJOR 1)
set(STRAWBERRY_VERSION_MINOR 2)
set(STRAWBERRY_VERSION_PATCH 2)
set(STRAWBERRY_VERSION_PATCH 5)
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION OFF)

3
debian/clean vendored Normal file
View File

@@ -0,0 +1,3 @@
dist/scripts/maketarball.sh
CMakeCache.txt
CMakeFiles/

1
debian/compat vendored
View File

@@ -1 +0,0 @@
11

4
debian/control vendored
View File

@@ -2,7 +2,7 @@ Source: strawberry
Section: sound
Priority: optional
Maintainer: Jonas Kvinge <jonas@jkvinge.net>
Build-Depends: debhelper (>= 11),
Build-Depends: debhelper-compat (= 12),
git,
make,
cmake,
@@ -28,7 +28,7 @@ Build-Depends: debhelper (>= 11),
libchromaprint-dev,
libfftw3-dev,
libebur128-dev
Standards-Version: 4.6.1
Standards-Version: 4.7.0
Package: strawberry
Architecture: any

15
debian/rules vendored
View File

@@ -1,17 +1,10 @@
#!/usr/bin/make -f
%:
dh $@ --buildsystem=cmake -builddirectory=build
override_dh_auto_clean:
rm -f dist/macos/Info.plist
rm -f dist/unix/strawberry.spec
rm -f dist/scripts/maketarball.sh
rm -f dist/windows/strawberry.nsi
rm -f src/translations/translations.pot
dh_auto_clean
export DH_VERBOSE=1
export DEB_BUILD_MAINT_OPTIONS=hardening=+all
override_dh_installchangelogs:
dh_installchangelogs Changelog
override_dh_auto_test:
%:
dh $@

14
dist/CMakeLists.txt vendored
View File

@@ -1,7 +1,7 @@
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh.in ${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh @ONLY)
if(RPM_DISTRO AND RPM_DATE)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec.in ${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec @ONLY)
endif(RPM_DISTRO AND RPM_DATE)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec.in ${CMAKE_BINARY_DIR}/strawberry.spec @ONLY)
endif()
if(APPLE)
if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET})
@@ -9,13 +9,13 @@ if(APPLE)
else()
set(LSMinimumSystemVersion 12.0)
endif()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist)
endif(APPLE)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
endif()
if(WIN32)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/windres.rc.in ${CMAKE_BINARY_DIR}/windres.rc)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi.in ${CMAKE_BINARY_DIR}/strawberry.nsi @ONLY)
endif(WIN32)
endif()
if(UNIX AND NOT APPLE)
install(FILES ../data/icons/48x48/strawberry.png DESTINATION share/icons/hicolor/48x48/apps/)
@@ -24,9 +24,9 @@ if(UNIX AND NOT APPLE)
install(FILES unix/org.strawberrymusicplayer.strawberry.desktop DESTINATION share/applications)
install(FILES unix/org.strawberrymusicplayer.strawberry.appdata.xml DESTINATION share/metainfo)
install(FILES unix/strawberry.1 DESTINATION share/man/man1)
endif(UNIX AND NOT APPLE)
endif()
if(APPLE)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/macos/strawberry.icns" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources")
endif()

View File

@@ -35,9 +35,9 @@
<key>LSMinimumSystemVersion</key>
<string>@LSMinimumSystemVersion@</string>
<key>SUFeedURL</key>
<string>https://www.strawberrymusicplayer.org/sparkle-macos</string>
<string>https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@</string>
<key>SUPublicEDKey</key>
<string>3IRScV8YtNVnx7zoeJAXvg28Kh1gN/Pyl2iPM467pG8=</string>
<string>/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=</string>
<key>CFBundleURLTypes</key>
<array>
<dict>

View File

@@ -51,6 +51,9 @@
</screenshots>
<update_contact>eclipseo@fedoraproject.org</update_contact>
<releases>
<release version="1.2.5" date="2025-01-17"/>
<release version="1.2.4" date="2025-01-10"/>
<release version="1.2.3" date="2024-12-08"/>
<release version="1.2.2" date="2024-11-23"/>
<release version="1.2.1" date="2024-11-21"/>
<release version="1.1.3" date="2024-09-21"/>

View File

@@ -63,6 +63,8 @@ BuildRequires: pkgconfig(libcdio)
BuildRequires: pkgconfig(libebur128)
BuildRequires: pkgconfig(libgpod-1.0)
BuildRequires: pkgconfig(libmtp)
BuildRequires: cmake(GTest)
BuildRequires: pkgconfig(gmock)
%if 0%{?suse_version}
Requires: qt6-sql-sqlite
@@ -103,13 +105,13 @@ Features:
%build
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
export CXXFLAGS="-fPIC $RPM_OPT_FLAGS"
export CXXFLAGS="-fPIC -Wno-maybe-uninitialized $RPM_OPT_FLAGS"
%endif
%if "%{?_vendor}" == "openmandriva"
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@ -DENABLE_TRANSLATIONS=OFF
%{cmake} -DBUILD_WERROR=ON
%make_build
%else
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@
%{cmake} -DBUILD_WERROR=ON
%cmake_build
%endif
@@ -120,11 +122,13 @@ Features:
%cmake_install
%endif
%if 0%{?suse_version}
%suse_update_desktop_file org.strawberrymusicplayer.strawberry Qt AudioVideo Audio Player
%endif
%check
export QT_QPA_PLATFORM="offscreen"
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
%{cmake_build} -t strawberry_tests
%else
%{make_build} -j $(nproc) -C build strawberry_tests
%endif
desktop-file-validate %{buildroot}%{_datadir}/applications/org.strawberrymusicplayer.strawberry.desktop
%if 0%{?suse_version}
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/org.strawberrymusicplayer.strawberry.appdata.xml

View File

@@ -208,14 +208,16 @@ FunctionEnd
!ifdef msvc
!define vc_redist_file "vc_redist.${arch}.exe"
Function InstallMSVCRuntime
${registry::Read} "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${arch}" "Version" $R0 $R1
${If} $R0 == ""
SetOutPath "$TEMP"
File "${vc_redist_file}"
; ${registry::Read} "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${arch}" "Version" $R0 $R1
; ${If} $R0 == ""
SetDetailsView hide
inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
; inetc::get /caption "Downloading..." "https://aka.ms/vs/17/release/${vc_redist_file}" "$TEMP\${vc_redist_file}" /end
ExecWait '"$TEMP\${vc_redist_file}" /install /passive'
Delete "$TEMP\${vc_redist_file}"
SetDetailsView show
${EndIf}
; ${EndIf}
FunctionEnd
!endif

View File

@@ -50,14 +50,14 @@
#include <QPixmapCache>
#include <QNetworkDiskCache>
#include <QSettings>
#include <QStandardPaths>
#include <QTimer>
#include "includes/scoped_ptr.h"
#include "includes/shared_ptr.h"
#include "core/logging.h"
#include "core/standardpaths.h"
#include "core/database.h"
#include "core/iconloader.h"
#include "core/logging.h"
#include "core/settings.h"
#include "core/songmimedata.h"
#include "collectionfilteroptions.h"
@@ -114,7 +114,7 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
pixmap_no_cover_ = nocover.pixmap(nocover_sizes.last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
icon_disk_cache_->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/' + QLatin1String(kPixmapDiskCacheDir) + u'-' + Song::TextForSource(backend_->source()));
icon_disk_cache_->setCacheDirectory(StandardPaths::WritableLocation(StandardPaths::StandardLocation::CacheLocation) + u'/' + QLatin1String(kPixmapDiskCacheDir) + u'-' + Song::TextForSource(backend_->source()));
QObject::connect(&*backend_, &CollectionBackend::SongsAdded, this, &CollectionModel::AddReAddOrUpdate);
QObject::connect(&*backend_, &CollectionBackend::SongsChanged, this, &CollectionModel::AddReAddOrUpdate);
@@ -288,29 +288,11 @@ void CollectionModel::SetFilterMaxAge(const int filter_max_age) {
QVariant CollectionModel::data(const QModelIndex &idx, const int role) const {
const CollectionItem *item = IndexToItem(idx);
// Handle a special case for returning album artwork instead of a generic CD icon.
// this is here instead of in the other data() function to let us use the
// QModelIndex& version of GetChildSongs, which satisfies const-ness, instead
// of the CollectionItem *version, which doesn't.
if (options_active_.show_pretty_covers) {
bool is_album_node = false;
if (role == Qt::DecorationRole && item->type == CollectionItem::Type::Container) {
GroupBy container_group_by = options_active_.group_by[item->container_level];
is_album_node = IsAlbumGroupBy(container_group_by);
}
if (is_album_node) {
// It has const behaviour some of the time - that's ok right?
return const_cast<CollectionModel*>(this)->AlbumIcon(idx);
}
}
return data(item, role);
return data(IndexToItem(idx), role);
}
QVariant CollectionModel::data(const CollectionItem *item, const int role) const {
QVariant CollectionModel::data(CollectionItem *item, const int role) const {
GroupBy container_group_by = item->type == CollectionItem::Type::Container ? options_active_.group_by[item->container_level] : GroupBy::None;
@@ -329,7 +311,7 @@ QVariant CollectionModel::data(const CollectionItem *item, const int role) const
case GroupBy::YearAlbumDisc:
case GroupBy::OriginalYearAlbum:
case GroupBy::OriginalYearAlbumDisc:
return QVariant();
return options_active_.show_pretty_covers ? const_cast<CollectionModel*>(this)->AlbumIcon(item) : QVariant();
case GroupBy::Artist:
case GroupBy::AlbumArtist:
return icon_artist_;
@@ -408,17 +390,17 @@ QMimeData *CollectionModel::mimeData(const QModelIndexList &indexes) const {
if (indexes.isEmpty()) return nullptr;
SongMimeData *data = new SongMimeData;
QList<QUrl> urls;
SongList songs;
QSet<int> song_ids;
data->backend = backend_;
QList<QUrl> urls;
for (const QModelIndex &idx : indexes) {
GetChildSongs(IndexToItem(idx), &urls, &data->songs, &song_ids);
GetChildSongs(IndexToItem(idx), songs, song_ids, urls);
}
SongMimeData *data = new SongMimeData;
data->setUrls(urls);
data->backend = backend_;
data->songs = songs;
data->name_for_new_playlist_ = Song::GetNameForNewPlaylist(data->songs);
return data;
@@ -854,9 +836,9 @@ void CollectionModel::LoadSongsFromSqlAsyncFinished() {
}
QString CollectionModel::AlbumIconPixmapCacheKey(const QModelIndex &idx) const {
QString CollectionModel::AlbumIconPixmapCacheKey(const CollectionItem *item) const {
return Song::TextForSource(backend_->source()) + QLatin1Char('/') + idx.data(Role_ContainerKey).toString();
return Song::TextForSource(backend_->source()) + QLatin1Char('/') + item->container_key;
}
@@ -869,7 +851,7 @@ QUrl CollectionModel::AlbumIconPixmapDiskCacheKey(const QString &cache_key) {
void CollectionModel::ClearItemPixmapCache(CollectionItem *item) {
// Remove from pixmap cache
const QString cache_key = AlbumIconPixmapCacheKey(ItemToIndex(item));
const QString cache_key = AlbumIconPixmapCacheKey(item);
QPixmapCache::remove(cache_key);
if (use_disk_cache_ && icon_disk_cache_) icon_disk_cache_->remove(AlbumIconPixmapDiskCacheKey(cache_key));
if (pending_cache_keys_.contains(cache_key)) {
@@ -888,13 +870,12 @@ void CollectionModel::ClearItemPixmapCache(CollectionItem *item) {
}
QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) {
QVariant CollectionModel::AlbumIcon(CollectionItem *item) {
CollectionItem *item = IndexToItem(idx);
if (!item) return pixmap_no_cover_;
// Check the cache for a pixmap we already loaded.
const QString cache_key = AlbumIconPixmapCacheKey(idx);
const QString cache_key = AlbumIconPixmapCacheKey(item);
QPixmap cached_pixmap;
if (QPixmapCache::find(cache_key, &cached_pixmap)) {
@@ -919,7 +900,7 @@ QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) {
}
// No art is cached and we're not loading it already. Load art for the first song in the album.
SongList songs = GetChildSongs(idx);
const SongList songs = GetChildSongs(item);
if (!songs.isEmpty()) {
AlbumCoverLoaderOptions cover_loader_options(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage);
cover_loader_options.desired_scaled_size = QSize(kPrettyCoverSize, kPrettyCoverSize);
@@ -1095,7 +1076,7 @@ QString CollectionModel::SortText(const GroupBy group_by, const Song &song, cons
case GroupBy::Artist:
return SortTextForArtist(song.artist(), sort_skips_articles);
case GroupBy::Album:
return SortTextForArtist(song.album(), sort_skips_articles);
return SortText(song.album());
case GroupBy::AlbumDisc:
return song.album() + SortTextForNumber(std::max(0, song.disc()));
case GroupBy::YearAlbum:
@@ -1412,7 +1393,7 @@ QString CollectionModel::DividerDisplayText(const GroupBy group_by, const QStrin
}
bool CollectionModel::CompareItems(const CollectionItem *a, const CollectionItem *b) const {
bool CollectionModel::CompareItems(CollectionItem *a, CollectionItem *b) const {
QVariant left = data(a, CollectionModel::Role_SortText);
QVariant right = data(b, CollectionModel::Role_SortText);
@@ -1453,24 +1434,23 @@ qint64 CollectionModel::MaximumCacheSize(Settings *s, const char *size_id, const
}
void CollectionModel::GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const {
void CollectionModel::GetChildSongs(CollectionItem *item, SongList &songs, QSet<int> &song_ids, QList<QUrl> &urls) const {
switch (item->type) {
case CollectionItem::Type::Container: {
QList<CollectionItem*> children = item->children;
std::sort(children.begin(), children.end(), std::bind(&CollectionModel::CompareItems, this, std::placeholders::_1, std::placeholders::_2));
for (CollectionItem *child : children) {
GetChildSongs(child, urls, songs, song_ids);
GetChildSongs(child, songs, song_ids, urls);
}
break;
}
case CollectionItem::Type::Song:
urls->append(item->metadata.url());
if (!song_ids->contains(item->metadata.id())) {
songs->append(item->metadata);
song_ids->insert(item->metadata.id());
urls << item->metadata.url();
if (!song_ids.contains(item->metadata.id())) {
songs << item->metadata;
song_ids << item->metadata.id();
}
break;
@@ -1480,16 +1460,33 @@ void CollectionModel::GetChildSongs(CollectionItem *item, QList<QUrl> *urls, Son
}
SongList CollectionModel::GetChildSongs(const QList<CollectionItem*> items) const {
SongList songs;
QSet<int> song_ids;
QList<QUrl> urls;
for (CollectionItem *item : items) {
GetChildSongs(item, songs, song_ids, urls);
}
return songs;
}
SongList CollectionModel::GetChildSongs(CollectionItem *item) const {
return GetChildSongs(QList<CollectionItem*>() << item);
}
SongList CollectionModel::GetChildSongs(const QModelIndexList &indexes) const {
QList<QUrl> dontcare;
SongList ret;
SongList songs;
QSet<int> song_ids;
QList<QUrl> urls;
for (const QModelIndex &idx : indexes) {
GetChildSongs(IndexToItem(idx), &dontcare, &ret, &song_ids);
GetChildSongs(IndexToItem(idx), songs, song_ids, urls);
}
return ret;
return songs;
}

View File

@@ -194,11 +194,13 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
QString ContainerKey(const GroupBy group_by, const Song &song, bool &has_unique_album_identifier) const;
// Get information about the collection
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
void GetChildSongs(CollectionItem *item, SongList &songs, QSet<int> &song_ids, QList<QUrl> &urls) const;
SongList GetChildSongs(const QList<CollectionItem*> items) const;
SongList GetChildSongs(CollectionItem *item) const;
SongList GetChildSongs(const QModelIndex &idx) const;
SongList GetChildSongs(const QModelIndexList &indexes) const;
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
bool CompareItems(CollectionItem *a, CollectionItem *b) const;
bool HasParentAlbumGroupBy(CollectionItem *item) const;
@@ -224,7 +226,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
void BeginReset();
void EndReset();
QVariant data(const CollectionItem *item, const int role) const;
QVariant data(CollectionItem *item, const int role) const;
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
void ScheduleAddSongs(const SongList &songs);
@@ -250,9 +252,9 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
// Helpers
static bool IsCompilationArtistNode(const CollectionItem *node) { return node == node->parent->compilation_artist_node_; }
QString AlbumIconPixmapCacheKey(const QModelIndex &idx) const;
QString AlbumIconPixmapCacheKey(const CollectionItem *item) const;
static QUrl AlbumIconPixmapDiskCacheKey(const QString &cache_key);
QVariant AlbumIcon(const QModelIndex &idx);
QVariant AlbumIcon(CollectionItem *item);
void ClearItemPixmapCache(CollectionItem *item);
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,6 +27,7 @@
#include <QObject>
#include <QThread>
#include <QIODevice>
#include <QStorageInfo>
#include <QDir>
#include <QDirIterator>
#include <QFile>
@@ -51,6 +52,7 @@
#include "core/settings.h"
#include "utilities/imageutils.h"
#include "constants/timeconstants.h"
#include "constants/filesystemconstants.h"
#include "tagreader/tagreaderclient.h"
#include "collectiondirectory.h"
#include "collectionbackend.h"
@@ -464,6 +466,24 @@ CollectionSubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
void CollectionWatcher::AddDirectory(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdirs) {
{
const QFileInfo path_info(dir.path);
if (path_info.isSymbolicLink()) {
const QStorageInfo storage_info(path_info.symLinkTarget());
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
qLog(Warning) << "Ignoring collection directory path" << dir.path << "which is a symbolic link to path" << path_info.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
return;
}
}
else {
const QStorageInfo storage_info(dir.path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
qLog(Warning) << "Ignoring collection directory path" << dir.path << "with rejected filesystem type" << storage_info.fileSystemType();
return;
}
}
}
CancelStop();
watched_dirs_[dir.id] = dir;
@@ -506,17 +526,29 @@ 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) {
QFileInfo path_info(path);
const QFileInfo path_info(path);
// Do not scan symlinked dirs that are already in collection
if (path_info.isSymLink()) {
QString real_path = path_info.symLinkTarget();
const QString real_path = path_info.symLinkTarget();
const QStorageInfo storage_info(real_path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
qLog(Warning) << "Ignoring symbolic link" << path << "which links to" << real_path << "with rejected filesystem type" << storage_info.fileSystemType();
return;
}
// Do not scan symlinked dirs that are already in collection
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
if (real_path.startsWith(dir.path)) {
return;
}
}
}
else {
const QStorageInfo storage_info(path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
qLog(Warning) << "Ignoring path" << path << "with rejected filesystem type" << storage_info.fileSystemType();
return;
}
}
bool songs_missing_fingerprint = false;
bool songs_missing_loudness_characteristics = false;
@@ -556,32 +588,40 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
if (stop_or_abort_requested()) return;
QString child(it.next());
QFileInfo child_info(child);
const QString child_filepath = it.next();
const QFileInfo child_fileinfo(child_filepath);
if (child_info.isDir()) {
if (!t->HasSeenSubdir(child)) {
if (child_fileinfo.isSymLink()) {
QStorageInfo storage_info(child_fileinfo.symLinkTarget());
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
qLog(Warning) << "Ignoring symbolic link" << child_filepath << "which links to" << child_fileinfo.symLinkTarget() << "with rejected filesystem type" << storage_info.fileSystemType();
continue;
}
}
if (child_fileinfo.isDir()) {
if (!t->HasSeenSubdir(child_filepath)) {
// We haven't seen this subdirectory before - add it to a list, and later we'll tell the backend about it and scan it.
CollectionSubdirectory new_subdir;
new_subdir.directory_id = -1;
new_subdir.path = child;
new_subdir.mtime = child_info.lastModified().toSecsSinceEpoch();
new_subdir.path = child_filepath;
new_subdir.mtime = child_fileinfo.lastModified().toSecsSinceEpoch();
my_new_subdirs << new_subdir;
}
t->AddToProgress(1);
}
else {
QString ext_part(ExtensionPart(child));
QString dir_part(DirectoryPart(child));
if (Song::kRejectedExtensions.contains(child_info.suffix(), Qt::CaseInsensitive) || child_info.baseName() == "qt_temp"_L1) {
QString ext_part(ExtensionPart(child_filepath));
QString dir_part(DirectoryPart(child_filepath));
if (Song::kRejectedExtensions.contains(child_fileinfo.suffix(), Qt::CaseInsensitive) || child_fileinfo.baseName() == "qt_temp"_L1) {
t->AddToProgress(1);
}
else if (sValidImages.contains(ext_part)) {
album_art[dir_part] << child;
album_art[dir_part] << child_filepath;
t->AddToProgress(1);
}
else if (tagreader_client_->IsMediaFileBlocking(child)) {
files_on_disk << child;
else if (tagreader_client_->IsMediaFileBlocking(child_filepath)) {
files_on_disk << child_filepath;
}
else {
t->AddToProgress(1);
@@ -828,7 +868,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
qLog(Error) << "Could not open CUE file" << matching_cue << "for reading:" << cue_file.errorString();
return;
}
const SongList songs = cue_parser_->Load(&cue_file, matching_cue, path, false);
const SongList songs = cue_parser_->Load(&cue_file, matching_cue, path, false).songs;
cue_file.close();
// Update every song that's in the CUE and collection
@@ -915,7 +955,7 @@ SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path
// Also, watch out for incorrect media files.
// Playlist parser for CUEs considers every entry in sheet valid, and we don't want invalid media getting into collection!
QString file_nfd = file.normalized(QString::NormalizationForm_D);
SongList cue_songs = cue_parser_->Load(&cue_file, matching_cue, path, false);
SongList cue_songs = cue_parser_->Load(&cue_file, matching_cue, path, false).songs;
cue_file.close();
songs.reserve(cue_songs.count());
for (Song &cue_song : cue_songs) {
@@ -1153,7 +1193,7 @@ void CollectionWatcher::RescanPathsNow() {
QMap<QString, quint64> subdir_files_count;
for (const QString &path : paths) {
quint64 files_count = FilesCountForPath(&transaction, path);
const quint64 files_count = FilesCountForPath(&transaction, path);
subdir_files_count[path] = files_count;
transaction.AddToProgressMax(files_count);
}
@@ -1316,18 +1356,45 @@ void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mt
quint64 CollectionWatcher::FilesCountForPath(ScanTransaction *t, const QString &path) {
const QFileInfo path_info(path);
if (path_info.isSymLink()) {
const QString real_path = path_info.symLinkTarget();
const QStorageInfo storage_info(real_path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
return 0;
}
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
if (real_path.startsWith(dir.path)) {
return 0;
}
}
}
else {
const QStorageInfo storage_info(path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
return 0;
}
}
quint64 i = 0;
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);
while (it.hasNext()) {
if (stop_or_abort_requested()) break;
QString child = it.next();
QFileInfo path_info(child);
const QString child_filepath = it.next();
const QFileInfo child_fileinfo(child_filepath);
if (child_fileinfo.isDir()) {
if (child_fileinfo.isSymLink()) {
const QString real_path = child_fileinfo.symLinkTarget();
QStorageInfo storage_info(real_path);
if (kRejectedFileSystems.contains(storage_info.fileSystemType())) {
continue;
}
if (path_info.isDir()) {
if (path_info.isSymLink()) {
QString real_path = path_info.symLinkTarget();
for (const CollectionDirectory &dir : std::as_const(watched_dirs_)) {
if (real_path.startsWith(dir.path)) {
continue;
@@ -1335,9 +1402,9 @@ quint64 CollectionWatcher::FilesCountForPath(ScanTransaction *t, const QString &
}
}
if (!t->HasSeenSubdir(child) && !path_info.isHidden()) {
if (!t->HasSeenSubdir(child_filepath) && !child_fileinfo.isHidden()) {
// We haven't seen this subdirectory before, so we need to include the file count for this directory too.
i += FilesCountForPath(t, child);
i += FilesCountForPath(t, child_filepath);
}
}
@@ -1385,7 +1452,7 @@ void CollectionWatcher::RescanSongs(const SongList &songs) {
if (stop_or_abort_requested()) break;
if (subdir.path != song_path) continue;
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
quint64 files_count = FilesCountForPath(&transaction, subdir.path);
const quint64 files_count = FilesCountForPath(&transaction, subdir.path);
ScanSubdirectory(song_path, subdir, files_count, &transaction);
scanned_paths << subdir.path;
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

View File

@@ -18,6 +18,7 @@
#cmakedefine HAVE_AUDIOCD
#cmakedefine HAVE_MTP
#cmakedefine HAVE_GPOD
#cmakedefine HAVE_SPARKLE
#cmakedefine HAVE_QTSPARKLE
#cmakedefine HAVE_SONGFINGERPRINTING
#cmakedefine HAVE_MUSICBRAINZ

View File

@@ -0,0 +1,31 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef FILESYSTEMCONSTANTS_H
#define FILESYSTEMCONSTANTS_H
#include <QByteArrayList>
const QByteArrayList kRejectedFileSystems = QByteArrayList() << "proc"
<< "sysfs"
<< "tmpfs"
<< "devtmpfs";
#endif // FILESYSTEMCONSTANTS_H

View File

@@ -37,6 +37,7 @@ constexpr char kPassword[] = "password";
constexpr char kHTTP2[] = "http2";
constexpr char kVerifyCertificate[] = "verifycertificate";
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
constexpr char kUseAlbumIdForAlbumCovers[] = "usealbumidforalbumcovers";
constexpr char kServerSideScrobbling[] = "serversidescrobbling";
constexpr char kAuthMethod[] = "authmethod";

View File

@@ -39,10 +39,10 @@
#include <QSqlDriver>
#include <QSqlDatabase>
#include <QSqlError>
#include <QStandardPaths>
#include <QScopeGuard>
#include "core/logging.h"
#include "logging.h"
#include "standardpaths.h"
#include "taskmanager.h"
#include "database.h"
#include "sqlquery.h"
@@ -78,7 +78,7 @@ Database::Database(SharedPtr<TaskManager> task_manager, QObject *parent, const Q
connection_id_ = sNextConnectionId++;
}
directory_ = QDir::toNativeSeparators(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
directory_ = QDir::toNativeSeparators(StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation)).replace(u"Strawberry"_s, u"strawberry"_s);
QMutexLocker l(&mutex_);
Connect();

View File

@@ -27,10 +27,10 @@
#include <QString>
#include <QIcon>
#include <QSize>
#include <QStandardPaths>
#include <QSettings>
#include "logging.h"
#include "standardpaths.h"
#include "settings.h"
#include "includes/iconmapper.h"
#include "iconloader.h"
@@ -51,7 +51,7 @@ void IconLoader::Init() {
#endif
QDir dir;
if (dir.exists(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/icons"_s)) {
if (dir.exists(StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/icons"_s)) {
custom_icons_ = true;
}
@@ -125,7 +125,7 @@ QIcon IconLoader::Load(const QString &name, const bool system_icon, const int fi
}
if (custom_icons_) {
QString custom_icon_path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/icons/%1x%2/%3.png"_s;
QString custom_icon_path = StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/icons/%1x%2/%3.png"_s;
for (int s : std::as_const(sizes)) {
QString filename(custom_icon_path.arg(s).arg(s).arg(name));
if (QFile::exists(filename)) ret.addFile(filename, QSize(s, s));

View File

@@ -220,6 +220,10 @@
# include "systemtrayicon/qtsystemtrayicon.h"
#endif
#ifdef HAVE_SPARKLE
#include "core/sparkleupdater.h"
#endif
#ifdef HAVE_QTSPARKLE
# include <qtsparkle-qt6/Updater>
#endif // HAVE_QTSPARKLE
@@ -235,20 +239,41 @@ const int kTrackPositionUpdateTimeMs = 1000;
} // namespace
#ifdef HAVE_QTSPARKLE
# ifdef _MSC_VER
# ifdef _M_X64
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-msvc-x64";
namespace {
# if defined(__APPLE__)
# if defined(__x86_64__)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-macos-x86_64";
# elif defined(__aarch64__)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-macos-arm64";
# else
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-msvc-x86";
# error "Unsupported macOS arch for QtSparkle"
# endif
# else
# ifdef __x86_64__
# elif defined(__MINGW32__)
# if defined(__x86_64__)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-mingw-x64";
# else
# elif defined(__i686__)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-mingw-x86";
# else
# error "Unsupported MinGW arch for QtSparkle"
# endif
# endif
#endif
# elif defined(_MSC_VER)
# if defined(_WIN64)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-msvc-x64";
# elif defined(_WIN32)
constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-windows-msvc-x86";
# else
# error "Unsupported MSVC arch for QtSparkle"
# endif
# else
# error "Unsupported OS for QtSparkle"
# endif // OS
} // namespace
#endif // HAVE_QTSPARKLE
MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd, const CommandlineOptions &options, QWidget *parent)
: QMainWindow(parent),
@@ -833,9 +858,9 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
thumbbar_->SetActions(QList<QAction*>() << ui_->action_previous_track << ui_->action_play_pause << ui_->action_stop << ui_->action_next_track << nullptr << ui_->action_love);
#endif
#if defined(HAVE_QTSPARKLE)
QAction *check_updates = ui_->menu_tools->addAction(tr("Check for updates..."));
check_updates->setMenuRole(QAction::ApplicationSpecificRole);
#if defined(HAVE_SPARKLE) || defined(HAVE_QTSPARKLE)
QAction *action_check_updates = ui_->menu_tools->addAction(tr("Check for updates..."));
action_check_updates->setMenuRole(QAction::ApplicationSpecificRole);
#endif
#ifdef HAVE_GLOBALSHORTCUTS
@@ -1046,13 +1071,18 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
app_->scrobbler()->Submit();
}
#ifdef HAVE_SPARKLE
SparkleUpdater *sparkle_updater = new SparkleUpdater(action_check_updates, this);
QObject::connect(action_check_updates, &QAction::triggered, sparkle_updater, &SparkleUpdater::CheckForUpdates);
#endif
#ifdef HAVE_QTSPARKLE
QUrl sparkle_url(QString::fromLatin1(QTSPARKLE_URL));
if (!sparkle_url.isEmpty()) {
qLog(Debug) << "Creating Qt Sparkle updater";
qtsparkle::Updater *updater = new qtsparkle::Updater(sparkle_url, this);
updater->SetVersion(QStringLiteral(STRAWBERRY_VERSION_PACKAGE));
QObject::connect(check_updates, &QAction::triggered, updater, &qtsparkle::Updater::CheckNow);
QObject::connect(action_check_updates, &QAction::triggered, updater, &qtsparkle::Updater::CheckNow);
}
#endif

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,13 +18,26 @@
*/
#include <QSettings>
#include <QVariant>
#include <QString>
#include <QCoreApplication>
#include "settings.h"
using namespace Qt::Literals::StringLiterals;
Settings::Settings(QObject *parent)
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
: QSettings(QCoreApplication::organizationName().toLower(), QCoreApplication::applicationName().toLower(), parent) {}
#else
: QSettings(parent) {}
#endif
Settings::Settings(const QSettings::Scope scope, QObject *parent)
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
: QSettings(scope, QCoreApplication::organizationName().toLower(), QCoreApplication::applicationName().toLower(), parent) {}
#else
: QSettings(scope, parent) {}
#endif
Settings::Settings(const QString &filename, const Format format, QObject *parent)
: QSettings(filename, format, parent) {}

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,14 +21,14 @@
#define SETTINGS_H
#include <QSettings>
#include <QObject>
#include <QVariant>
#include <QString>
class Settings : public QSettings {
Q_OBJECT
public:
explicit Settings(QObject *parent = nullptr);
explicit Settings(const QSettings::Scope scope, QObject *parent = nullptr);
explicit Settings(const QString &filename, const Format format, QObject *parent = nullptr);
};

View File

@@ -26,7 +26,8 @@
#include <QVariant>
#include <QString>
#include <QSettings>
#include "core/settings.h"
class SettingsProvider {
public:
@@ -60,7 +61,7 @@ class DefaultSettingsProvider : public SettingsProvider {
void endArray() override;
private:
QSettings backend_;
Settings backend_;
Q_DISABLE_COPY(DefaultSettingsProvider)
};

View File

@@ -45,11 +45,11 @@
#include <QRegularExpression>
#include <QUrl>
#include <QIcon>
#include <QStandardPaths>
#include <QSqlRecord>
#include <taglib/tstring.h>
#include "core/standardpaths.h"
#include "core/iconloader.h"
#include "core/enginemetadata.h"
#include "utilities/strutils.h"
@@ -249,7 +249,8 @@ const QStringList Song::kRejectedExtensions = QStringList() << u"tmp"_s
<< u"z"_s
<< u"zip"_s
<< u"rar"_s
<< u"wvc"_s;
<< u"wvc"_s
<< u"zst"_s;
struct Song::Private : public QSharedData {
@@ -1334,24 +1335,24 @@ QString Song::ImageCacheDir(const Source source) {
switch (source) {
case Source::Collection:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/collectionalbumcovers"_s;
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/collectionalbumcovers"_s;
case Source::Subsonic:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/subsonicalbumcovers"_s;
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/subsonicalbumcovers"_s;
case Source::Tidal:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/tidalalbumcovers"_s;
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/tidalalbumcovers"_s;
case Source::Spotify:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/spotifyalbumcovers"_s;
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/spotifyalbumcovers"_s;
case Source::Qobuz:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/qobuzalbumcovers"_s;
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/qobuzalbumcovers"_s;
case Source::Device:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/devicealbumcovers"_s;
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/devicealbumcovers"_s;
case Source::LocalFile:
case Source::CDDA:
case Source::Stream:
case Source::SomaFM:
case Source::RadioParadise:
case Source::Unknown:
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/albumcovers"_s;
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/albumcovers"_s;
}
return QString();

View File

@@ -281,7 +281,7 @@ SongLoader::Result SongLoader::LoadLocalAsync(const QString &filename) {
// It's a CUE - create virtual tracks
QFile cue(matching_cue);
if (cue.open(QIODevice::ReadOnly)) {
const SongList songs = cue_parser_->Load(&cue, matching_cue, QDir(filename.section(u'/', 0, -2)));
const SongList songs = cue_parser_->Load(&cue, matching_cue, QDir(filename.section(u'/', 0, -2))).songs;
cue.close();
for (const Song &song : songs) {
if (song.is_valid()) songs_ << song;
@@ -348,7 +348,9 @@ void SongLoader::LoadPlaylist(ParserBase *parser, const QString &filename) {
QFile file(filename);
if (file.open(QIODevice::ReadOnly)) {
songs_ = parser->Load(&file, filename, QFileInfo(filename).path());
const ParserBase::LoadResult result = parser->Load(&file, filename, QFileInfo(filename).path());
songs_ = result.songs;
playlist_name_ = result.playlist_name;
file.close();
}
else {
@@ -424,7 +426,9 @@ void SongLoader::StopTypefind() {
// Parse the playlist
QBuffer buf(&buffer_);
if (buf.open(QIODevice::ReadOnly)) {
songs_ = parser_->Load(&buf);
const ParserBase::LoadResult result = parser_->Load(&buf);
songs_ = result.songs;
playlist_name_ = result.playlist_name;
buf.close();
}

View File

@@ -72,6 +72,7 @@ class SongLoader : public QObject {
const QUrl &url() const { return url_; }
const SongList &songs() const { return songs_; }
const QString &playlist_name() const { return playlist_name_; }
int timeout() const { return timeout_; }
void set_timeout(int msec) { timeout_ = msec; }
@@ -141,6 +142,7 @@ class SongLoader : public QObject {
QUrl url_;
SongList songs_;
QString playlist_name_;
const SharedPtr<UrlHandlers> url_handlers_;
const SharedPtr<CollectionBackendInterface> collection_backend_;

48
src/core/sparkleupdater.h Normal file
View File

@@ -0,0 +1,48 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SPARKLEUPDATER_H
#define SPARKLEUPDATER_H
#include <QObject>
class QAction;
#ifdef __OBJC__
@class AppUpdaterDelegate;
#endif
class SparkleUpdater : public QObject {
Q_OBJECT
public:
explicit SparkleUpdater(QAction *action_check_updates, QObject *parent = nullptr);
public Q_SLOTS:
void CheckForUpdates();
private:
#ifdef __OBJC__
AppUpdaterDelegate *updater_delegate_;
#else
void *updater_delegate_;
#endif
};
#endif // SPARKLEUPDATER_H

View File

@@ -0,0 +1,79 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#import <Sparkle/Sparkle.h>
#include <QObject>
#include <QAction>
#include "sparkleupdater.h"
@interface AppUpdaterDelegate : NSObject <SPUUpdaterDelegate>
@property(nonatomic, assign) SPUStandardUpdaterController *updater_controller;
@end
@implementation AppUpdaterDelegate
- (void)observeCanCheckForUpdatesWithAction:(QAction*)action_check_updates {
[_updater_controller.updater addObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates)) options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:(void*)action_check_updates];
}
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id>*)change context:(void*)context {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(canCheckForUpdates))]) {
QAction *action = reinterpret_cast<QAction*>(context);
action->setEnabled(_updater_controller.updater.canCheckForUpdates);
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc {
@autoreleasepool {
[_updater_controller.updater removeObserver:self forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates))];
}
[super dealloc];
}
@end
SparkleUpdater::SparkleUpdater(QAction *action_check_updates, QObject *parent) : QObject(parent) {
@autoreleasepool {
updater_delegate_ = [[AppUpdaterDelegate alloc] init];
updater_delegate_.updater_controller = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:YES updaterDelegate:updater_delegate_ userDriverDelegate:nil];
[updater_delegate_ observeCanCheckForUpdatesWithAction:action_check_updates];
}
}
void SparkleUpdater::CheckForUpdates() {
@autoreleasepool {
[updater_delegate_.updater_controller checkForUpdates:nil];
}
}

View File

@@ -0,0 +1,82 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QtGlobal>
#include <QString>
#include <QDir>
#include <QCoreApplication>
#include "standardpaths.h"
using namespace Qt::StringLiterals;
void StandardPaths::AppendOrganizationAndApplication(QString &path) {
const QString organization_name = QCoreApplication::organizationName().toLower();
if (!organization_name.isEmpty()) {
path += u'/' + organization_name;
}
const QString application_name = QCoreApplication::applicationName().toLower();
if (!application_name.isEmpty()) {
path += u'/' + application_name;
}
}
QString StandardPaths::WritableLocation(const StandardLocation type) {
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
switch (type) {
case StandardLocation::CacheLocation:
case StandardLocation::GenericCacheLocation:{
QString cache_location = qEnvironmentVariable("XDG_CACHE_HOME");
if (!cache_location.startsWith(u'/')) {
cache_location.clear();
}
if (cache_location.isEmpty()) {
cache_location = QDir::homePath() + "/.cache"_L1;
}
if (type == QStandardPaths::CacheLocation) {
AppendOrganizationAndApplication(cache_location);
}
return cache_location;
}
case StandardLocation::AppDataLocation:
case StandardLocation::AppLocalDataLocation:
case StandardLocation::GenericDataLocation:{
QString data_location = qEnvironmentVariable("XDG_DATA_HOME");
if (!data_location.startsWith(u'/')) {
data_location.clear();
}
if (data_location.isEmpty()) {
data_location = QDir::homePath() + "/.local/share"_L1;
}
if (type == StandardLocation::AppDataLocation || type == StandardLocation::AppLocalDataLocation) {
AppendOrganizationAndApplication(data_location);
}
return data_location;
}
default:
break;
}
#endif
return QStandardPaths::writableLocation(type);
}

36
src/core/standardpaths.h Normal file
View File

@@ -0,0 +1,36 @@
/*
* Strawberry Music Player
* Copyright 2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef STANDARDPATHS_H
#define STANDARDPATHS_H
#include <QStandardPaths>
class StandardPaths {
Q_GADGET
public:
using StandardLocation = QStandardPaths::StandardLocation;
static QString WritableLocation(const StandardLocation type);
private:
static void AppendOrganizationAndApplication(QString &path);
};
#endif // STANDARDPATHS_H

View File

@@ -24,7 +24,6 @@
#include <QtGlobal>
#include <QObject>
#include <QCoreApplication>
#include <QStandardPaths>
#include <QIODevice>
#include <QMutex>
#include <QNetworkDiskCache>
@@ -32,6 +31,7 @@
#include <QAbstractNetworkCache>
#include <QUrl>
#include "standardpaths.h"
#include "threadsafenetworkdiskcache.h"
using namespace Qt::Literals::StringLiterals;
@@ -48,9 +48,9 @@ ThreadSafeNetworkDiskCache::ThreadSafeNetworkDiskCache(QObject *parent) : QAbstr
if (!sCache) {
sCache = new QNetworkDiskCache;
#ifdef Q_OS_WIN32
sCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + u"/strawberry/networkcache"_s);
sCache->setCacheDirectory(StandardPaths::WritableLocation(StandardPaths::StandardLocation::TempLocation) + u"/strawberry/networkcache"_s);
#else
sCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u"/networkcache"_s);
sCache->setCacheDirectory(StandardPaths::WritableLocation(StandardPaths::StandardLocation::CacheLocation) + u"/networkcache"_s);
#endif
}

View File

@@ -27,9 +27,9 @@
#include <QObject>
#include <QDir>
#include <QImage>
#include <QStandardPaths>
#include "core/logging.h"
#include "core/standardpaths.h"
#include "core/song.h"
#include "core/temporaryfile.h"
#include "albumcoverloader.h"
@@ -42,7 +42,7 @@ using namespace Qt::Literals::StringLiterals;
CurrentAlbumCoverLoader::CurrentAlbumCoverLoader(const SharedPtr<AlbumCoverLoader> albumcover_loader, QObject *parent)
: QObject(parent),
albumcover_loader_(albumcover_loader),
temp_file_pattern_(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + u"/strawberry-cover-XXXXXX.jpg"_s),
temp_file_pattern_(StandardPaths::WritableLocation(StandardPaths::StandardLocation::TempLocation) + u"/strawberry-cover-XXXXXX.jpg"_s),
id_(0) {
setObjectName(QLatin1String(metaObject()->className()));

View File

@@ -52,10 +52,12 @@
using namespace Qt::Literals::StringLiterals;
using std::make_shared;
const char *DiscogsCoverProvider::kUrlSearch = "https://api.discogs.com/database/search";
const char *DiscogsCoverProvider::kAccessKeyB64 = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk=";
const char *DiscogsCoverProvider::kSecretKeyB64 = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI=";
const int DiscogsCoverProvider::kRequestsDelay = 1000;
namespace {
constexpr char kUrlSearch[] = "https://api.discogs.com/database/search";
constexpr char kAccessKeyB64[] = "dGh6ZnljUGJlZ1NEeXBuSFFxSVk=";
constexpr char kSecretKeyB64[] = "ZkFIcmlaSER4aHhRSlF2U3d0bm5ZVmdxeXFLWUl0UXI=";
constexpr int kRequestsDelay = 1000;
} // namespace
DiscogsCoverProvider::DiscogsCoverProvider(const SharedPtr<NetworkAccessManager> network, QObject *parent)
: JsonCoverProvider(u"Discogs"_s, false, false, 0.0, false, false, network, parent),

View File

@@ -89,11 +89,6 @@ class DiscogsCoverProvider : public JsonCoverProvider {
void HandleReleaseReply(QNetworkReply *reply, const int search_id, const quint64 release_id);
private:
static const char *kUrlSearch;
static const char *kAccessKeyB64;
static const char *kSecretKeyB64;
static const int kRequestsDelay;
QTimer *timer_flush_requests_;
QQueue<SharedPtr<DiscogsCoverSearchContext>> queue_search_requests_;
QQueue<DiscogsCoverReleaseContext> queue_release_requests_;

View File

@@ -24,9 +24,6 @@
#include "config.h"
#include <cdio/cdio.h>
#include <gst/audio/gstaudiocdsrc.h>
#include <QObject>
#include <QString>
#include <QStringList>

View File

@@ -21,6 +21,11 @@
#include <config.h>
#include <cstddef>
#include <cdio/cdio.h>
#include <cdio/device.h>
#include <QtGlobal>
#include <QFileInfo>
#include <QByteArray>
@@ -30,10 +35,6 @@
#include <QRegularExpression>
#include <QUrl>
// This must come after Qt includes
#include <cdio/cdio.h>
#include <cdio/device.h>
#include "cddalister.h"
#include "core/logging.h"

View File

@@ -24,10 +24,16 @@
#include <memory>
#include <cstddef>
#include <glib.h>
#include <glib/gtypes.h>
#include <glib-object.h>
#include <cdio/cdio.h>
#include <gst/gst.h>
#include <gst/tag/tag.h>
#include <QtGlobal>
#include <QObject>
#include <QMutex>
@@ -35,10 +41,6 @@
#include <QString>
#include <QUrl>
#include <cdio/cdio.h>
#include <gst/gst.h>
#include <gst/tag/tag.h>
#include "cddasongloader.h"
#include "includes/shared_ptr.h"
#include "core/logging.h"

View File

@@ -24,17 +24,19 @@
#include "config.h"
#include <cstddef>
#include <cdio/types.h>
#include <cdio/cdio.h>
#include <gst/gstelement.h>
#include <gst/audio/gstaudiocdsrc.h>
#include <QObject>
#include <QMutex>
#include <QString>
#include <QUrl>
// These must come after Qt includes
#include <cdio/types.h>
#include <cdio/cdio.h>
#include <gst/gstelement.h>
#include <gst/audio/gstaudiocdsrc.h>
#include "includes/shared_ptr.h"
#include "core/song.h"
#ifdef HAVE_MUSICBRAINZ

View File

@@ -484,16 +484,22 @@ void GioLister::DeviceInfo::ReadMountInfo(GMount *mount) {
}
#ifdef HAVE_GIO_UNIX
#ifdef GLIB_VERSION_2_84
GUnixMountEntry *unix_mount = g_unix_mount_entry_for(g_file_get_path(root), nullptr);
#else
GUnixMountEntry *unix_mount = g_unix_mount_for(g_file_get_path(root), nullptr);
#endif
if (unix_mount) {
// the GIO's definition of system internal mounts include filesystems like
// autofs, tmpfs, sysfs, etc, and various system directories, including the root,
// /boot, /var, /home, etc.
// The GIO's definition of system internal mounts include filesystems like autofs, tmpfs, sysfs, etc,
// and various system directories, including the root, /boot, /var, /home, etc.
#ifdef GLIB_VERSION_2_84
is_system_internal = g_unix_mount_entry_is_system_internal(unix_mount);
g_unix_mount_entry_free(unix_mount);
#else
is_system_internal = g_unix_mount_is_system_internal(unix_mount);
g_unix_mount_free(unix_mount);
// Although checking most of the internal mounts is safe,
// we really don't want to touch autofs filesystems, as that would
// trigger automounting.
#endif
// Although checking most of the internal mounts is safe, we really don't want to touch autofs filesystems, as that would trigger automounting.
if (is_system_internal) return;
}
#endif

View File

@@ -36,10 +36,10 @@
#include <QString>
#include <QUrl>
#include <QImage>
#include <QStandardPaths>
#include "includes/shared_ptr.h"
#include "core/logging.h"
#include "core/standardpaths.h"
#include "core/temporaryfile.h"
#include "core/taskmanager.h"
#include "core/database.h"
@@ -211,9 +211,9 @@ bool GPodDevice::CopyToStorage(const CopyJob &job, QString &error_text) {
bool result = false;
if (!job.cover_image_.isNull()) {
#ifdef Q_OS_LINUX
QString temp_path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u"/organize"_s;
QString temp_path = StandardPaths::WritableLocation(StandardPaths::StandardLocation::CacheLocation) + u"/organize"_s;
#else
QString temp_path = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
QString temp_path = StandardPaths::WritableLocation(StandardPaths::StandardLocation::TempLocation);
#endif
if (!QDir(temp_path).exists()) QDir().mkpath(temp_path);
SharedPtr<TemporaryFile> cover_file = make_shared<TemporaryFile>(temp_path + u"/track-albumcover-XXXXXX.jpg"_s);

View File

@@ -49,8 +49,8 @@ EngineBase::EngineBase(QObject *parent)
exclusive_mode_(false),
volume_control_(true),
volume_(100),
beginning_nanosec_(0),
end_nanosec_(0),
beginning_offset_nanosec_(0),
end_offset_nanosec_(0),
ebur128_loudness_normalizing_gain_db_(0.0),
scope_(kScopeSize),
buffering_(false),
@@ -84,15 +84,15 @@ EngineBase::EngineBase(QObject *parent)
EngineBase::~EngineBase() = default;
bool EngineBase::Load(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags track_change_flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional<double> ebur128_integrated_loudness_lufs) {
bool EngineBase::Load(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags track_change_flags, const bool force_stop_at_end, const quint64 beginning_offset_nanosec, const qint64 end_offset_nanosec, const std::optional<double> ebur128_integrated_loudness_lufs) {
Q_UNUSED(track_change_flags)
Q_UNUSED(force_stop_at_end);
media_url_ = media_url;
stream_url_ = stream_url;
beginning_nanosec_ = beginning_nanosec;
end_nanosec_ = end_nanosec;
beginning_offset_nanosec_ = beginning_offset_nanosec;
end_offset_nanosec_ = end_offset_nanosec;
ebur128_loudness_normalizing_gain_db_ = 0.0;
if (ebur128_loudness_normalization_ && ebur128_integrated_loudness_lufs) {
@@ -112,9 +112,9 @@ bool EngineBase::Load(const QUrl &media_url, const QUrl &stream_url, const Track
}
bool EngineBase::Play(const QUrl &media_url, const QUrl &stream_url, const bool pause, const TrackChangeFlags flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const quint64 offset_nanosec, const std::optional<double> ebur128_integrated_loudness_lufs) {
bool EngineBase::Play(const QUrl &media_url, const QUrl &stream_url, const bool pause, const TrackChangeFlags flags, const bool force_stop_at_end, const quint64 beginning_offset_nanosec, const qint64 end_offset_nanosec, const quint64 offset_nanosec, const std::optional<double> ebur128_integrated_loudness_lufs) {
if (!Load(media_url, stream_url, flags, force_stop_at_end, beginning_nanosec, end_nanosec, ebur128_integrated_loudness_lufs)) {
if (!Load(media_url, stream_url, flags, force_stop_at_end, beginning_offset_nanosec, end_offset_nanosec, ebur128_integrated_loudness_lufs)) {
return false;
}

View File

@@ -91,7 +91,7 @@ class EngineBase : public QObject {
virtual bool Init() = 0;
virtual State state() const = 0;
virtual void StartPreloading(const QUrl&, const QUrl&, const bool, const qint64, const qint64) {}
virtual bool Load(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags track_change_flags, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional<double> ebur128_integrated_loudness_lufs);
virtual bool Load(const QUrl &media_url, const QUrl &stream_url, const TrackChangeFlags track_change_flags, const bool force_stop_at_end, const quint64 beginning_offset_nanosec, const qint64 end_offset_nanosec, const std::optional<double> ebur128_integrated_loudness_lufs);
virtual bool Play(const bool pause, const quint64 offset_nanosec) = 0;
virtual void Stop(const bool stop_after = false) = 0;
virtual void Pause() = 0;
@@ -106,9 +106,9 @@ class EngineBase : public QObject {
// Sets new values for the beginning and end markers of the currently playing song.
// This doesn't change the state of engine or the stream's current position.
virtual void RefreshMarkers(const quint64 beginning_nanosec, const qint64 end_nanosec) {
beginning_nanosec_ = beginning_nanosec;
end_nanosec_ = end_nanosec;
virtual void RefreshMarkers(const quint64 beginning_offset_nanosec, const qint64 end_offset_nanosec) {
beginning_offset_nanosec_ = beginning_offset_nanosec;
end_offset_nanosec_ = end_offset_nanosec;
}
virtual OutputDetailsList GetOutputsList() const = 0;
@@ -179,8 +179,8 @@ class EngineBase : public QObject {
bool exclusive_mode_;
bool volume_control_;
uint volume_;
quint64 beginning_nanosec_;
qint64 end_nanosec_;
quint64 beginning_offset_nanosec_;
qint64 end_offset_nanosec_;
QUrl media_url_;
QUrl stream_url_;
double ebur128_loudness_normalizing_gain_db_;

View File

@@ -62,7 +62,6 @@
#include "gstbufferconsumer.h"
using namespace Qt::Literals::StringLiterals;
using std::make_shared;
#ifdef __clang__
# pragma clang diagnostic push
@@ -177,13 +176,13 @@ EngineBase::State GstEngine::state() const {
}
void GstEngine::StartPreloading(const QUrl &media_url, const QUrl &stream_url, const bool force_stop_at_end, const qint64 beginning_nanosec, const qint64 end_nanosec) {
void GstEngine::StartPreloading(const QUrl &media_url, const QUrl &stream_url, const bool force_stop_at_end, const qint64 beginning_offset_nanosec, const qint64 end_offset_nanosec) {
const QByteArray gst_url = FixupUrl(stream_url);
// No crossfading, so we can just queue the new URL in the existing pipeline and get gapless playback (hopefully)
if (current_pipeline_) {
current_pipeline_->PrepareNextUrl(media_url, stream_url, gst_url, beginning_nanosec, force_stop_at_end ? end_nanosec : 0);
current_pipeline_->PrepareNextUrl(media_url, stream_url, gst_url, beginning_offset_nanosec, force_stop_at_end ? end_offset_nanosec : 0);
// Add request to discover the stream
if (discoverer_ && media_url.scheme() != u"spotify"_s) {
if (!gst_discoverer_discover_uri_async(discoverer_, gst_url.constData())) {
@@ -194,9 +193,9 @@ void GstEngine::StartPreloading(const QUrl &media_url, const QUrl &stream_url, c
}
bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional<double> ebur128_integrated_loudness_lufs) {
bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_offset_nanosec, const qint64 end_offset_nanosec, const std::optional<double> ebur128_integrated_loudness_lufs) {
EngineBase::Load(media_url, stream_url, change, force_stop_at_end, beginning_nanosec, end_nanosec, ebur128_integrated_loudness_lufs);
EngineBase::Load(media_url, stream_url, change, force_stop_at_end, beginning_offset_nanosec, end_offset_nanosec, ebur128_integrated_loudness_lufs);
const QByteArray gst_url = FixupUrl(stream_url);
@@ -215,7 +214,7 @@ bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const Engine
}
}
GstEnginePipelinePtr pipeline = CreatePipeline(media_url, stream_url, gst_url, force_stop_at_end ? end_nanosec : 0, ebur128_loudness_normalizing_gain_db_);
GstEnginePipelinePtr pipeline = CreatePipeline(media_url, stream_url, gst_url, static_cast<qint64>(beginning_offset_nanosec), force_stop_at_end ? end_offset_nanosec : 0, ebur128_loudness_normalizing_gain_db_);
if (!pipeline) return false;
GstEnginePipelinePtr old_pipeline = current_pipeline_;
@@ -264,7 +263,17 @@ bool GstEngine::Load(const QUrl &media_url, const QUrl &stream_url, const Engine
bool GstEngine::Play(const bool pause, const quint64 offset_nanosec) {
if (!current_pipeline_ || current_pipeline_->is_buffering() || current_pipeline_->state() == GstState::GST_STATE_PLAYING) return false;
if (!current_pipeline_ || current_pipeline_->is_buffering()) {
return false;
}
if (current_pipeline_->state() == GstState::GST_STATE_PLAYING) {
if (offset_nanosec != 0 || beginning_offset_nanosec_ != 0) {
Seek(offset_nanosec);
PlayDone(GST_STATE_CHANGE_SUCCESS, false, offset_nanosec, current_pipeline_->id());
}
return true;
}
if (OldExclusivePipelineActive()) {
qLog(Debug) << "Delaying play because a exclusive pipeline is already active...";
@@ -289,7 +298,7 @@ bool GstEngine::Play(const bool pause, const quint64 offset_nanosec) {
watcher->deleteLater();
PlayDone(ret, pause, offset_nanosec, pipeline_id);
});
QFuture<GstStateChangeReturn> future = current_pipeline_->Play(pause, beginning_nanosec_ + offset_nanosec);
QFuture<GstStateChangeReturn> future = current_pipeline_->Play(pause, beginning_offset_nanosec_ + offset_nanosec);
watcher->setFuture(future);
return true;
@@ -306,7 +315,7 @@ void GstEngine::Stop(const bool stop_after) {
media_url_.clear();
stream_url_.clear(); // To ensure we return Empty from state()
beginning_nanosec_ = end_nanosec_ = 0;
beginning_offset_nanosec_ = end_offset_nanosec_ = 0;
// Check if we started a fade out. If it isn't finished yet and the user pressed stop, we cancel the fader and just stop the playback.
if (fadeout_pause_pipeline_) {
@@ -384,7 +393,7 @@ void GstEngine::Seek(const quint64 offset_nanosec) {
if (!current_pipeline_) return;
seek_pos_ = beginning_nanosec_ + offset_nanosec;
seek_pos_ = beginning_offset_nanosec_ + offset_nanosec;
waiting_to_seek_ = true;
if (!seek_timer_->isActive()) {
@@ -402,7 +411,7 @@ qint64 GstEngine::position_nanosec() const {
if (!current_pipeline_) return 0;
const qint64 result = current_pipeline_->position() - static_cast<qint64>(beginning_nanosec_);
const qint64 result = current_pipeline_->position() - static_cast<qint64>(beginning_offset_nanosec_);
return std::max(0LL, result);
}
@@ -411,7 +420,7 @@ qint64 GstEngine::length_nanosec() const {
if (!current_pipeline_) return 0;
const qint64 result = end_nanosec_ - static_cast<qint64>(beginning_nanosec_);
const qint64 result = end_offset_nanosec_ - static_cast<qint64>(beginning_offset_nanosec_);
if (result > 0) {
return result;
@@ -742,7 +751,7 @@ void GstEngine::PlayDone(const GstStateChangeReturn ret, const bool pause, const
stream_url = old_pipeline->stream_url();
stream_url.detach();
}
current_pipeline_ = CreatePipeline(media_url, stream_url, redirect_url, end_nanosec_, old_pipeline->ebur128_loudness_normalizing_gain_db());
current_pipeline_ = CreatePipeline(media_url, stream_url, redirect_url, beginning_offset_nanosec_, end_offset_nanosec_, old_pipeline->ebur128_loudness_normalizing_gain_db());
FinishPipeline(old_pipeline);
Play(pause, offset_nanosec);
return;
@@ -761,7 +770,7 @@ void GstEngine::PlayDone(const GstStateChangeReturn ret, const bool pause, const
Q_EMIT StateChanged(pause ? State::Paused : State::Playing);
// We've successfully started playing a media stream with this url
// We've successfully started playing a media stream with this URL
Q_EMIT ValidSongRequested(stream_url_);
}
@@ -887,7 +896,7 @@ void GstEngine::StopTimers() {
GstEnginePipelinePtr GstEngine::CreatePipeline() {
GstEnginePipelinePtr pipeline = make_shared<GstEnginePipeline>();
GstEnginePipelinePtr pipeline = GstEnginePipelinePtr(new GstEnginePipeline);
pipeline->set_output_device(output_, device_);
pipeline->set_exclusive_mode(exclusive_mode_);
pipeline->set_volume_enabled(volume_control_);
@@ -926,11 +935,11 @@ GstEnginePipelinePtr GstEngine::CreatePipeline() {
}
GstEnginePipelinePtr GstEngine::CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db) {
GstEnginePipelinePtr GstEngine::CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_offset_nanosec, const qint64 end_offset_nanosec, const double ebur128_loudness_normalizing_gain_db) {
GstEnginePipelinePtr ret = CreatePipeline();
QString error;
if (!ret->InitFromUrl(media_url, stream_url, gst_url, end_nanosec, ebur128_loudness_normalizing_gain_db, error)) {
if (!ret->InitFromUrl(media_url, stream_url, gst_url, beginning_offset_nanosec, end_offset_nanosec, ebur128_loudness_normalizing_gain_db, error)) {
ret.reset();
Q_EMIT Error(error);
Q_EMIT StateChanged(State::Error);

View File

@@ -60,8 +60,8 @@ class GstEngine : public EngineBase, public GstBufferConsumer {
bool Init() override;
State state() const override;
void StartPreloading(const QUrl &media_url, const QUrl &stream_url, const bool force_stop_at_end, const qint64 beginning_nanosec, const qint64 end_nanosec) override;
bool Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec, const std::optional<double> ebur128_integrated_loudness_lufs) override;
void StartPreloading(const QUrl &media_url, const QUrl &stream_url, const bool force_stop_at_end, const qint64 beginning_offset_nanosec, const qint64 end_offset_nanosec) override;
bool Load(const QUrl &media_url, const QUrl &stream_url, const EngineBase::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_offset_nanosec, const qint64 end_offset_nanosec, const std::optional<double> ebur128_integrated_loudness_lufs) override;
bool Play(const bool pause, const quint64 offset_nanosec) override;
void Stop(const bool stop_after = false) override;
void Pause() override;
@@ -133,7 +133,7 @@ class GstEngine : public EngineBase, public GstBufferConsumer {
void StopTimers();
GstEnginePipelinePtr CreatePipeline();
GstEnginePipelinePtr CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db);
GstEnginePipelinePtr CreatePipeline(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_offset_nanosec, const qint64 end_offset_nanosec, const double ebur128_loudness_normalizing_gain_db);
void FinishPipeline(GstEnginePipelinePtr pipeline);

View File

@@ -125,6 +125,7 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent)
ebur128_loudness_normalizing_gain_db_(0.0),
segment_start_(0),
segment_start_received_(false),
beginning_offset_nanosec_(-1),
end_offset_nanosec_(-1),
next_beginning_offset_nanosec_(-1),
next_end_offset_nanosec_(-1),
@@ -173,7 +174,11 @@ GstEnginePipeline::GstEnginePipeline(QObject *parent)
logged_unsupported_analyzer_format_(false),
about_to_finish_(false),
finish_requested_(false),
finished_(false) {
finished_(false),
set_state_in_progress_(0),
set_state_async_in_progress_(0),
last_set_state_in_progress_(GST_STATE_VOID_PENDING),
last_set_state_async_in_progress_(GST_STATE_VOID_PENDING) {
eq_band_gains_.reserve(kEqBandCount);
for (int i = 0; i < kEqBandCount; ++i) eq_band_gains_ << 0;
@@ -418,18 +423,23 @@ bool GstEnginePipeline::Finish() {
Disconnect();
if (IsStateNull()) {
if (IsStateNull() && set_state_async_in_progress_ == 0 && set_state_in_progress_ == 0) {
finished_ = true;
}
else {
SetState(GST_STATE_NULL);
if (set_state_async_in_progress_ > 0 && last_set_state_async_in_progress_ != GST_STATE_NULL) {
SetStateAsync(GST_STATE_NULL);
}
else if ((!IsStateNull() || set_state_in_progress_ > 0) && last_set_state_in_progress_ != GST_STATE_NULL) {
SetState(GST_STATE_NULL);
}
}
return finished_.value();
}
bool GstEnginePipeline::InitFromUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db, QString &error) {
bool GstEnginePipeline::InitFromUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_offset_nanosec, const qint64 end_offset_nanosec, const double ebur128_loudness_normalizing_gain_db, QString &error) {
{
QMutexLocker l(&mutex_url_);
@@ -438,7 +448,8 @@ bool GstEnginePipeline::InitFromUrl(const QUrl &media_url, const QUrl &stream_ur
gst_url_ = gst_url;
}
end_offset_nanosec_ = end_nanosec;
beginning_offset_nanosec_ = beginning_offset_nanosec;
end_offset_nanosec_ = end_offset_nanosec;
ebur128_loudness_normalizing_gain_db_ = ebur128_loudness_normalizing_gain_db;
guint version_major = 0, version_minor = 0, version_micro = 0, version_nano = 0;
@@ -1328,6 +1339,7 @@ GstPadProbeReturn GstEnginePipeline::BufferProbeCallback(GstPad *pad, GstPadProb
if (instance->end_offset_nanosec_.value() > 0 && end_time > instance->end_offset_nanosec_.value()) {
if (instance->HasMatchingNextUrl() && instance->next_beginning_offset_nanosec_.value() == instance->end_offset_nanosec_.value()) {
// The "next" song is actually the next segment of this file - so cheat and keep on playing, but just tell the Engine we've moved on.
instance->beginning_offset_nanosec_ = instance->next_beginning_offset_nanosec_;
instance->end_offset_nanosec_ = instance->next_end_offset_nanosec_;
instance->next_media_url_.clear();
instance->next_stream_url_.clear();
@@ -1477,6 +1489,7 @@ void GstEnginePipeline::StreamStartMessageReceived() {
next_media_url_.clear();
next_gst_url_.clear();
}
beginning_offset_nanosec_ = next_beginning_offset_nanosec_;
end_offset_nanosec_ = next_end_offset_nanosec_;
next_beginning_offset_nanosec_ = 0;
next_end_offset_nanosec_ = 0;
@@ -1788,7 +1801,19 @@ bool GstEnginePipeline::IsStateNull() const {
void GstEnginePipeline::SetStateAsync(const GstState state) {
QMetaObject::invokeMethod(this, "SetState", Qt::QueuedConnection, Q_ARG(GstState, state));
last_set_state_async_in_progress_ = state;
++set_state_async_in_progress_;
QMetaObject::invokeMethod(this, "SetStateAsyncSlot", Qt::QueuedConnection, Q_ARG(GstState, state));
}
void GstEnginePipeline::SetStateAsyncSlot(const GstState state) {
last_set_state_async_in_progress_ = GST_STATE_VOID_PENDING;
--set_state_async_in_progress_;
SetState(state);
}
@@ -1796,6 +1821,9 @@ QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(const GstState state)
qLog(Debug) << "Setting pipeline" << id() << "state to" << GstStateText(state);
last_set_state_in_progress_ = state;
++set_state_in_progress_;
QFutureWatcher<GstStateChangeReturn> *watcher = new QFutureWatcher<GstStateChangeReturn>();
QObject::connect(watcher, &QFutureWatcher<GstStateChangeReturn>::finished, this, [this, watcher, state]() {
const GstStateChangeReturn state_change_return = watcher->result();
@@ -1811,13 +1839,16 @@ QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(const GstState state)
void GstEnginePipeline::SetStateFinishedSlot(const GstState state, const GstStateChangeReturn state_change_return) {
last_set_state_in_progress_ = GST_STATE_VOID_PENDING;
--set_state_in_progress_;
switch (state_change_return) {
case GST_STATE_CHANGE_SUCCESS:
case GST_STATE_CHANGE_ASYNC:
case GST_STATE_CHANGE_NO_PREROLL:
qLog(Debug) << "Pipeline" << id() << "state successfully set to" << GstStateText(state);
Q_EMIT SetStateFinished(state_change_return);
if (!finished_.value() && finish_requested_.value()) {
if (!finished_.value() && finish_requested_.value() && set_state_async_in_progress_ == 0 && set_state_in_progress_ == 0) {
finished_ = true;
Q_EMIT Finished();
}
@@ -2137,7 +2168,7 @@ bool GstEnginePipeline::HasMatchingNextUrl() const {
}
void GstEnginePipeline::PrepareNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_nanosec, const qint64 end_nanosec) {
void GstEnginePipeline::PrepareNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_offset_nanosec, const qint64 end_offset_nanosec) {
{
QMutexLocker l(&mutex_next_url_);
@@ -2146,8 +2177,8 @@ void GstEnginePipeline::PrepareNextUrl(const QUrl &media_url, const QUrl &stream
next_gst_url_ = gst_url;
}
next_beginning_offset_nanosec_ = beginning_nanosec;
next_end_offset_nanosec_ = end_nanosec;
next_beginning_offset_nanosec_ = beginning_offset_nanosec;
next_end_offset_nanosec_ = end_offset_nanosec;
if (about_to_finish_.value()) {
SetNextUrl();

View File

@@ -41,6 +41,7 @@
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QSharedPointer>
#include "includes/shared_ptr.h"
#include "includes/mutex_protected.h"
@@ -83,7 +84,7 @@ class GstEnginePipeline : public QObject {
bool Finish();
// Creates the pipeline, returns false on error
bool InitFromUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 end_nanosec, const double ebur128_loudness_normalizing_gain_db, QString &error);
bool InitFromUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_offset_nanosec, const qint64 end_offset_nanosec, const double ebur128_loudness_normalizing_gain_db, QString &error);
// GstBufferConsumers get fed audio data. Thread-safe.
void AddBufferConsumer(GstBufferConsumer *consumer);
@@ -105,7 +106,7 @@ class GstEnginePipeline : public QObject {
// If this is set then it will be loaded automatically when playback finishes for gapless playback
bool HasNextUrl() const;
bool HasMatchingNextUrl() const;
void PrepareNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_nanosec, const qint64 end_nanosec);
void PrepareNextUrl(const QUrl &media_url, const QUrl &stream_url, const QByteArray &gst_url, const qint64 beginning_offset_nanosec, const qint64 end_offset_nanosec);
void SetNextUrl();
void SetSourceDevice(const QString &device);
@@ -197,6 +198,7 @@ class GstEnginePipeline : public QObject {
void ProcessPendingSeek(const GstState state);
private Q_SLOTS:
void SetStateAsyncSlot(const GstState state);
void SetStateFinishedSlot(const GstState state, const GstStateChangeReturn state_change_return);
void SetFaderVolume(const qreal volume);
void FaderTimelineStateChanged(const QTimeLine::State state);
@@ -287,6 +289,7 @@ class GstEnginePipeline : public QObject {
mutex_protected<bool> segment_start_received_;
GstSegment last_playbin_segment_{};
mutex_protected<qint64> beginning_offset_nanosec_;
// If this is > 0 then the pipeline will be forced to stop when playback goes past this position.
mutex_protected<qint64> end_offset_nanosec_;
@@ -367,8 +370,14 @@ class GstEnginePipeline : public QObject {
mutex_protected<bool> about_to_finish_;
mutex_protected<bool> finish_requested_;
mutex_protected<bool> finished_;
mutex_protected<int> set_state_in_progress_;
mutex_protected<int> set_state_async_in_progress_;
mutex_protected<GstState> last_set_state_in_progress_;
mutex_protected<GstState> last_set_state_async_in_progress_;
};
using GstEnginePipelinePtr = SharedPtr<GstEnginePipeline>;
using GstEnginePipelinePtr = QSharedPointer<GstEnginePipeline>;
#endif // GSTENGINEPIPELINE_H

View File

@@ -26,12 +26,12 @@
#include <gst/pbutils/pbutils.h>
#include <QCoreApplication>
#include <QStandardPaths>
#include <QString>
#include <QDir>
#include <QFile>
#include "core/logging.h"
#include "core/standardpaths.h"
#include "utilities/envutils.h"
#ifdef HAVE_MOODBAR
@@ -145,7 +145,7 @@ void SetEnvironment() {
#endif // USE_BUNDLE
#if defined(Q_OS_WIN32) || defined(Q_OS_MACOS)
QString gst_registry_filename = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QStringLiteral("/gst-registry-%1-bin").arg(QCoreApplication::applicationVersion());
QString gst_registry_filename = StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + QStringLiteral("/gst-registry-%1-bin").arg(QCoreApplication::applicationVersion());
qLog(Debug) << "Setting GStreamer registry file to" << gst_registry_filename;
Utilities::SetEnv("GST_REGISTRY", gst_registry_filename);
#endif

View File

@@ -188,8 +188,15 @@ FilterTree *FilterParser::parseSearchTerm() {
QString value;
bool in_quotes = false;
bool previous_char_operator = false;
for (; iter_ != end_; ++iter_) {
if (previous_char_operator) {
if (iter_->isSpace()) {
continue;
}
previous_char_operator = false;
}
if (in_quotes) {
if (*iter_ == u'"') {
in_quotes = false;
@@ -206,6 +213,7 @@ FilterTree *FilterParser::parseSearchTerm() {
column = buf_.toLower();
buf_.clear();
prefix.clear(); // Prefix isn't allowed here - let's ignore it
previous_char_operator = true;
}
else if (iter_->isSpace() || *iter_ == u'(' || *iter_ == u')' || *iter_ == u'-') {
break;
@@ -214,9 +222,11 @@ FilterTree *FilterParser::parseSearchTerm() {
// We don't know whether there is a column part in this search term thus we assume the latter and just try and read a prefix
if (prefix.isEmpty() && (*iter_ == u'>' || *iter_ == u'<' || *iter_ == u'=' || *iter_ == u'!')) {
prefix += *iter_;
previous_char_operator = true;
}
else if (prefix != u'=' && *iter_ == u'=') {
prefix += *iter_;
previous_char_operator = true;
}
else {
buf_ += *iter_;

View File

@@ -47,6 +47,56 @@ class mutex_protected : public boost::noncopyable {
return value == value_;
}
bool operator!=(const mutex_protected &value) const {
QMutexLocker l(&mutex_);
return value.value() != value_;
}
bool operator!=(const T value) const {
QMutexLocker l(&mutex_);
return value != value_;
}
bool operator>(const mutex_protected &value) const {
QMutexLocker l(&mutex_);
return value_ > value.value();
}
bool operator>(const T value) const {
QMutexLocker l(&mutex_);
return value_ > value;
}
bool operator>=(const mutex_protected &value) const {
QMutexLocker l(&mutex_);
return value_ >= value.value();
}
bool operator>=(const T value) const {
QMutexLocker l(&mutex_);
return value_ >= value;
}
bool operator<(const mutex_protected &value) const {
QMutexLocker l(&mutex_);
return value_ < value.value();
}
bool operator<(const T value) const {
QMutexLocker l(&mutex_);
return value_ < value;
}
bool operator<=(const mutex_protected &value) const {
QMutexLocker l(&mutex_);
return value_ <= value.value();
}
bool operator<=(const T value) const {
QMutexLocker l(&mutex_);
return value_ <= value;
}
void operator=(const mutex_protected &value) {
QMutexLocker l(&mutex_);
value_ = value.value();
@@ -57,6 +107,36 @@ class mutex_protected : public boost::noncopyable {
value_ = value;
}
void operator++() {
QMutexLocker l(&mutex_);
++value_;
}
void operator+=(const T value) {
QMutexLocker l(&mutex_);
value_ += value;
}
void operator+=(const mutex_protected value) {
QMutexLocker l(&mutex_);
value_ += value.value();
}
void operator--() {
QMutexLocker l(&mutex_);
--value_;
}
void operator-=(const T value) {
QMutexLocker l(&mutex_);
value_ -= value;
}
void operator-=(const mutex_protected value) {
QMutexLocker l(&mutex_);
value_ -= value.value();
}
private:
T value_;
mutable QMutex mutex_;

View File

@@ -48,7 +48,6 @@
#include <QApplication>
#include <QCoreApplication>
#include <QSysInfo>
#include <QStandardPaths>
#include <QLibraryInfo>
#include <QFileDevice>
#include <QIODevice>
@@ -70,6 +69,7 @@
#include "includes/shared_ptr.h"
#include "core/logging.h"
#include "core/standardpaths.h"
#include "core/settings.h"
#include "utilities/envutils.h"
@@ -131,13 +131,8 @@ int main(int argc, char *argv[]) {
mac::MacMain();
#endif
#if defined(Q_OS_WIN32) || defined(Q_OS_MACOS)
QCoreApplication::setApplicationName(u"Strawberry"_s);
QCoreApplication::setOrganizationName(u"Strawberry"_s);
#else
QCoreApplication::setApplicationName(u"strawberry"_s);
QCoreApplication::setOrganizationName(u"strawberry"_s);
#endif
QCoreApplication::setApplicationVersion(QStringLiteral(STRAWBERRY_VERSION_DISPLAY));
QCoreApplication::setOrganizationDomain(u"strawberrymusicplayer.org"_s);
@@ -157,7 +152,7 @@ int main(int argc, char *argv[]) {
// Only start a core application now, so we can check if there's another instance without requiring an X server.
// This MUST be done before parsing the commandline options so QTextCodec gets the right system locale for filenames.
QCoreApplication core_app(argc, argv);
KDSingleApplication single_app(QCoreApplication::applicationName(), KDSingleApplication::Option::IncludeUsernameInSocketName);
KDSingleApplication single_app(QCoreApplication::applicationName().toLower(), KDSingleApplication::Option::IncludeUsernameInSocketName);
// Parse commandline options - need to do this before starting the full QApplication, so it works without an X server
if (!options.Parse()) return 1;
logging::SetLevels(options.log_levels());
@@ -174,7 +169,7 @@ int main(int argc, char *argv[]) {
#ifdef Q_OS_MACOS
// Must happen after QCoreApplication::setOrganizationName().
Utilities::SetEnv("XDG_CONFIG_HOME", QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
Utilities::SetEnv("XDG_CONFIG_HOME", StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppConfigLocation));
#endif
// Output the version, so when people attach log output to bug reports they don't have to tell us which version they're using.
@@ -194,7 +189,7 @@ int main(int argc, char *argv[]) {
QGuiApplication::setQuitOnLastWindowClosed(false);
QApplication a(argc, argv);
KDSingleApplication single_app(QCoreApplication::applicationName(), KDSingleApplication::Option::IncludeUsernameInSocketName);
KDSingleApplication single_app(QCoreApplication::applicationName().toLower(), KDSingleApplication::Option::IncludeUsernameInSocketName);
if (!single_app.isPrimaryInstance()) {
if (options.is_empty()) {
qLog(Info) << "Strawberry is already running - activating existing window (2)";
@@ -295,7 +290,7 @@ int main(int argc, char *argv[]) {
translations->LoadTranslation(u"strawberry"_s, QDir::currentPath(), language);
# ifdef HAVE_QTSPARKLE
//qtsparkle::LoadTranslations(language);
qtsparkle::LoadTranslations(language);
# endif
#endif

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2019-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,6 +23,7 @@
#include <QByteArray>
#include <QUrl>
#include "includes/shared_ptr.h"
#include "core/song.h"
#include "core/settings.h"
#include "core/player.h"
@@ -33,6 +34,8 @@
#include "moodbarloader.h"
#include "moodbarpipeline.h"
using std::make_shared;
MoodbarController::MoodbarController(const SharedPtr<Player> player, const SharedPtr<MoodbarLoader> moodbar_loader, QObject *parent)
: QObject(parent),
player_(player),
@@ -56,25 +59,27 @@ void MoodbarController::CurrentSongChanged(const Song &song) {
if (!enabled_) return;
QByteArray data;
MoodbarPipeline *pipeline = nullptr;
const MoodbarLoader::Result result = moodbar_loader_->Load(song.url(), song.has_cue(), &data, &pipeline);
switch (result) {
case MoodbarLoader::Result::CannotLoad:
Q_EMIT CurrentMoodbarDataChanged(QByteArray());
const MoodbarLoader::LoadResult load_result = moodbar_loader_->Load(song.url(), song.has_cue());
switch (load_result.status) {
case MoodbarLoader::LoadStatus::CannotLoad:
Q_EMIT CurrentMoodbarDataChanged();
break;
case MoodbarLoader::Result::Loaded:
Q_EMIT CurrentMoodbarDataChanged(data);
case MoodbarLoader::LoadStatus::Loaded:
Q_EMIT CurrentMoodbarDataChanged(load_result.data);
break;
case MoodbarLoader::Result::WillLoadAsync:
// Emit an empty array for now so the GUI reverts to a normal progress
// bar. Our slot will be called when the data is actually loaded.
Q_EMIT CurrentMoodbarDataChanged(QByteArray());
case MoodbarLoader::LoadStatus::WillLoadAsync:
// Emit an empty array for now so the GUI reverts to a normal progressbar. Our slot will be called when the data is actually loaded.
Q_EMIT CurrentMoodbarDataChanged();
QObject::connect(pipeline, &MoodbarPipeline::Finished, this, [this, pipeline, song]() { AsyncLoadComplete(pipeline, song.url()); });
MoodbarPipelinePtr pipeline = load_result.pipeline;
Q_ASSERT(pipeline);
SharedPtr<QMetaObject::Connection> connection = make_shared<QMetaObject::Connection>();
*connection = QObject::connect(&*pipeline, &MoodbarPipeline::Finished, this, [this, connection, pipeline, song]() {
AsyncLoadComplete(pipeline, song.url());
QObject::disconnect(*connection);
});
break;
}
@@ -83,12 +88,12 @@ void MoodbarController::CurrentSongChanged(const Song &song) {
void MoodbarController::PlaybackStopped() {
if (enabled_) {
Q_EMIT CurrentMoodbarDataChanged(QByteArray());
Q_EMIT CurrentMoodbarDataChanged();
}
}
void MoodbarController::AsyncLoadComplete(MoodbarPipeline *pipeline, const QUrl &url) {
void MoodbarController::AsyncLoadComplete(MoodbarPipelinePtr pipeline, const QUrl &url) {
// Is this song still playing?
PlaylistItemPtr current_item = player_->GetCurrentItem();

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2019-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,9 +28,9 @@
#include <QUrl>
#include "includes/shared_ptr.h"
#include "moodbarpipeline.h"
class MoodbarLoader;
class MoodbarPipeline;
class Song;
class Player;
@@ -43,15 +43,15 @@ class MoodbarController : public QObject {
void ReloadSettings();
Q_SIGNALS:
void CurrentMoodbarDataChanged(const QByteArray &data);
void CurrentMoodbarDataChanged(const QByteArray &data = QByteArray());
void StyleChanged();
public Q_SLOTS:
void CurrentSongChanged(const Song &song);
void PlaybackStopped();
private Q_SLOTS:
void AsyncLoadComplete(MoodbarPipeline *pipeline, const QUrl &url);
private:
void AsyncLoadComplete(MoodbarPipelinePtr pipeline, const QUrl &url);
private:
const SharedPtr<Player> player_;

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2019-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -36,6 +36,8 @@
#include <QPainter>
#include <QRect>
#include "includes/shared_ptr.h"
#include "core/logging.h"
#include "core/settings.h"
#include "playlist/playlist.h"
#include "playlist/playlistview.h"
@@ -48,6 +50,8 @@
#include "constants/moodbarsettings.h"
using std::make_shared;
MoodbarItemDelegate::Data::Data() : state_(State::None) {}
MoodbarItemDelegate::MoodbarItemDelegate(const SharedPtr<MoodbarLoader> moodbar_loader, PlaylistView *playlist_view, QObject *parent)
@@ -113,7 +117,11 @@ QPixmap MoodbarItemDelegate::PixmapForIndex(const QModelIndex &idx, const QSize
}
else {
data = new Data;
if (!data_.insert(url, data)) return QPixmap();
if (!data_.insert(url, data)) {
qLog(Error) << "Could not insert moodbar data for URL" << url << "into cache";
delete data;
return QPixmap();
}
}
data->indexes_.insert(idx);
@@ -150,21 +158,24 @@ void MoodbarItemDelegate::StartLoadingData(const QUrl &url, const bool has_cue,
data->state_ = Data::State::LoadingData;
// Load a mood file for this song and generate some colors from it
QByteArray bytes;
MoodbarPipeline *pipeline = nullptr;
switch (moodbar_loader_->Load(url, has_cue, &bytes, &pipeline)) {
case MoodbarLoader::Result::CannotLoad:
const MoodbarLoader::LoadResult load_result = moodbar_loader_->Load(url, has_cue);
switch (load_result.status) {
case MoodbarLoader::LoadStatus::CannotLoad:
data->state_ = Data::State::CannotLoad;
break;
case MoodbarLoader::Result::Loaded:
// We got the data immediately.
StartLoadingColors(url, bytes, data);
case MoodbarLoader::LoadStatus::Loaded:
StartLoadingColors(url, load_result.data, data);
break;
case MoodbarLoader::Result::WillLoadAsync:
// Maybe in a little while.
QObject::connect(pipeline, &MoodbarPipeline::Finished, this, [this, url, pipeline]() { DataLoaded(url, pipeline); });
case MoodbarLoader::LoadStatus::WillLoadAsync:
MoodbarPipelinePtr pipeline = load_result.pipeline;
Q_ASSERT(pipeline);
SharedPtr<QMetaObject::Connection> connection = make_shared<QMetaObject::Connection>();
*connection = QObject::connect(&*pipeline, &MoodbarPipeline::Finished, this, [this, connection, url, pipeline]() {
DataLoaded(url, pipeline);
QObject::disconnect(*connection);
});
break;
}
@@ -194,7 +205,7 @@ void MoodbarItemDelegate::ReloadAllColors() {
}
void MoodbarItemDelegate::DataLoaded(const QUrl &url, MoodbarPipeline *pipeline) {
void MoodbarItemDelegate::DataLoaded(const QUrl &url, MoodbarPipelinePtr pipeline) {
if (!data_.contains(url)) return;
@@ -291,8 +302,7 @@ void MoodbarItemDelegate::ImageLoaded(const QUrl &url, const QImage &image) {
}
if (source_index.model() != playlist) {
// The pixmap was for an index in a different playlist, maybe the user
// switched to a different one.
// The pixmap was for an index in a different playlist, maybe the user switched to a different one.
continue;
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2019-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,10 +38,10 @@
#include "includes/shared_ptr.h"
#include "constants/moodbarsettings.h"
#include "moodbarpipeline.h"
class QPainter;
class MoodbarLoader;
class MoodbarPipeline;
class PlaylistView;
class MoodbarItemDelegate : public QItemDelegate {
@@ -55,13 +55,14 @@ class MoodbarItemDelegate : public QItemDelegate {
private Q_SLOTS:
void ReloadSettings();
void DataLoaded(const QUrl &url, MoodbarPipeline *pipeline);
void ColorsLoaded(const QUrl &url, const ColorVector &colors);
void ImageLoaded(const QUrl &url, const QImage &image);
Q_SIGNALS:
void StyleChanged();
private:
void DataLoaded(const QUrl &url, MoodbarPipelinePtr pipeline);
void ColorsLoaded(const QUrl &url, const ColorVector &colors);
void ImageLoaded(const QUrl &url, const QImage &image);
private:
struct Data {
Data();

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2019-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,21 +28,21 @@
#include <QObject>
#include <QThread>
#include <QCoreApplication>
#include <QStandardPaths>
#include <QIODevice>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QAbstractNetworkCache>
#include <QNetworkDiskCache>
#include <QTimer>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QSettings>
#include "includes/scoped_ptr.h"
#include "includes/shared_ptr.h"
#include "core/logging.h"
#include "core/standardpaths.h"
#include "core/settings.h"
#include "moodbarpipeline.h"
@@ -51,6 +51,7 @@
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
using std::make_shared;
#ifdef Q_OS_WIN32
# include <windows.h>
@@ -63,7 +64,10 @@ MoodbarLoader::MoodbarLoader(QObject *parent)
kMaxActiveRequests(qMax(1, QThread::idealThreadCount() / 2)),
save_(false) {
cache_->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u"/moodbar"_s);
setObjectName(QLatin1String(metaObject()->className()));
thread_->setObjectName(objectName());
cache_->setCacheDirectory(StandardPaths::WritableLocation(StandardPaths::StandardLocation::CacheLocation) + u"/moodbar"_s);
cache_->setMaximumCacheSize(60LL * 1024LL * 1024LL); // 60MB - enough for 20,000 moodbars
ReloadSettings();
@@ -104,16 +108,15 @@ QUrl MoodbarLoader::CacheUrlEntry(const QString &filename) {
}
MoodbarLoader::Result MoodbarLoader::Load(const QUrl &url, const bool has_cue, QByteArray *data, MoodbarPipeline **async_pipeline) {
MoodbarLoader::LoadResult MoodbarLoader::Load(const QUrl &url, const bool has_cue) {
if (!url.isLocalFile() || has_cue) {
return Result::CannotLoad;
return LoadStatus::CannotLoad;
}
// Are we in the middle of loading this moodbar already?
if (requests_.contains(url)) {
*async_pipeline = requests_.value(url);
return Result::WillLoadAsync;
return LoadResult(LoadStatus::WillLoadAsync, requests_.value(url));
}
// Check if a mood file exists for this file already
@@ -121,16 +124,18 @@ MoodbarLoader::Result MoodbarLoader::Load(const QUrl &url, const bool has_cue, Q
const QStringList possible_mood_files = MoodFilenames(filename);
for (const QString &possible_mood_file : possible_mood_files) {
QFile f(possible_mood_file);
if (f.exists()) {
if (f.open(QIODevice::ReadOnly)) {
QFile file(possible_mood_file);
if (file.exists()) {
if (file.open(QIODevice::ReadOnly)) {
qLog(Info) << "Loading moodbar data from" << possible_mood_file;
*data = f.readAll();
f.close();
return Result::Loaded;
const QByteArray data = file.readAll();
file.close();
if (!data.isEmpty()) {
return LoadResult(LoadStatus::Loaded, data);
}
}
else {
qLog(Error) << "Failed to load moodbar data from" << possible_mood_file << f.errorString();
qLog(Error) << "Failed to load moodbar data from" << possible_mood_file << file.errorString();
}
}
}
@@ -142,9 +147,9 @@ MoodbarLoader::Result MoodbarLoader::Load(const QUrl &url, const bool has_cue, Q
ScopedPtr<QIODevice> device_cache_file(cache_->data(disk_cache_metadata.url()));
if (device_cache_file) {
qLog(Info) << "Loading cached moodbar data for" << filename;
*data = device_cache_file->readAll();
if (!data->isEmpty()) {
return Result::Loaded;
const QByteArray data = device_cache_file->readAll();
if (!data.isEmpty()) {
return LoadResult(LoadStatus::Loaded, data);
}
}
}
@@ -152,17 +157,20 @@ MoodbarLoader::Result MoodbarLoader::Load(const QUrl &url, const bool has_cue, Q
if (!thread_->isRunning()) thread_->start(QThread::IdlePriority);
// There was no existing file, analyze the audio file and create one.
MoodbarPipeline *pipeline = new MoodbarPipeline(url);
MoodbarPipelinePtr pipeline = MoodbarPipelinePtr(new MoodbarPipeline(url));
pipeline->moveToThread(thread_);
QObject::connect(pipeline, &MoodbarPipeline::Finished, this, [this, pipeline, url]() { RequestFinished(pipeline, url); });
SharedPtr<QMetaObject::Connection> connection = make_shared<QMetaObject::Connection>();
*connection = QObject::connect(&*pipeline, &MoodbarPipeline::Finished, this, [this, connection, pipeline, url]() {
RequestFinished(pipeline, url);
QObject::disconnect(*connection);
});
requests_[url] = pipeline;
queued_requests_ << url;
MaybeTakeNextRequest();
*async_pipeline = pipeline;
return Result::WillLoadAsync;
return LoadResult(LoadStatus::WillLoadAsync, pipeline);
}
@@ -178,15 +186,17 @@ void MoodbarLoader::MaybeTakeNextRequest() {
active_requests_ << url;
qLog(Info) << "Creating moodbar data for" << url.toLocalFile();
QMetaObject::invokeMethod(requests_.value(url), &MoodbarPipeline::Start, Qt::QueuedConnection);
MoodbarPipelinePtr pipeline = requests_.value(url);
QMetaObject::invokeMethod(&*pipeline, &MoodbarPipeline::Start, Qt::QueuedConnection);
}
void MoodbarLoader::RequestFinished(MoodbarPipeline *request, const QUrl &url) {
void MoodbarLoader::RequestFinished(MoodbarPipelinePtr pipeline, const QUrl &url) {
Q_ASSERT(QThread::currentThread() == qApp->thread());
if (request->success()) {
if (pipeline->success()) {
const QString filename = url.toLocalFile();
@@ -201,7 +211,7 @@ void MoodbarLoader::RequestFinished(MoodbarPipeline *request, const QUrl &url) {
QIODevice *device_cache_file = cache_->prepare(disk_cache_metadata);
if (device_cache_file) {
const qint64 data_written = device_cache_file->write(request->data());
const qint64 data_written = device_cache_file->write(pipeline->data());
if (data_written > 0) {
cache_->insert(device_cache_file);
}
@@ -213,7 +223,7 @@ void MoodbarLoader::RequestFinished(MoodbarPipeline *request, const QUrl &url) {
const QString mood_filename(mood_filenames[0]);
QFile mood_file(mood_filename);
if (mood_file.open(QIODevice::WriteOnly)) {
if (mood_file.write(request->data()) <= 0) {
if (mood_file.write(pipeline->data()) <= 0) {
qLog(Error) << "Error writing to mood file" << mood_filename << mood_file.errorString();
}
mood_file.close();
@@ -233,8 +243,6 @@ void MoodbarLoader::RequestFinished(MoodbarPipeline *request, const QUrl &url) {
requests_.remove(url);
active_requests_.remove(url);
QTimer::singleShot(1s, request, &MoodbarLoader::deleteLater);
MaybeTakeNextRequest();
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2019-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,10 +30,11 @@
#include <QStringList>
#include <QUrl>
#include "moodbarpipeline.h"
class QThread;
class QByteArray;
class QNetworkDiskCache;
class MoodbarPipeline;
class MoodbarLoader : public QObject {
Q_OBJECT
@@ -42,7 +43,7 @@ class MoodbarLoader : public QObject {
explicit MoodbarLoader(QObject *parent = nullptr);
~MoodbarLoader() override;
enum class Result {
enum class LoadStatus {
// The URL isn't a local file or the moodbar plugin was not available -
// moodbar data can never be loaded.
CannotLoad,
@@ -55,17 +56,25 @@ class MoodbarLoader : public QObject {
WillLoadAsync
};
class LoadResult {
public:
LoadResult(const LoadStatus _status) : status(_status) {}
LoadResult(const LoadStatus _status, MoodbarPipelinePtr _pipeline) : status(_status), pipeline(_pipeline) {}
LoadResult(const LoadStatus _status, const QByteArray &_data) : status(_status), data(_data) {}
LoadStatus status;
MoodbarPipelinePtr pipeline;
QByteArray data;
};
void ReloadSettings();
Result Load(const QUrl &url, const bool has_cue, QByteArray *data, MoodbarPipeline **async_pipeline);
private Q_SLOTS:
void RequestFinished(MoodbarPipeline *request, const QUrl &url);
void MaybeTakeNextRequest();
LoadResult Load(const QUrl &url, const bool has_cue);
private:
static QStringList MoodFilenames(const QString &song_filename);
static QUrl CacheUrlEntry(const QString &filename);
void RequestFinished(MoodbarPipelinePtr pipeline, const QUrl &url);
void MaybeTakeNextRequest();
Q_SIGNALS:
void MoodbarEnabled(const bool enabled);
@@ -78,7 +87,7 @@ class MoodbarLoader : public QObject {
const int kMaxActiveRequests;
QMap<QUrl, MoodbarPipeline*> requests_;
QMap<QUrl, MoodbarPipelinePtr> requests_;
QList<QUrl> queued_requests_;
QSet<QUrl> active_requests_;

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2019-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -56,20 +56,24 @@ MoodbarPipeline::MoodbarPipeline(const QUrl &url, QObject *parent)
success_(false),
running_(false) {}
MoodbarPipeline::~MoodbarPipeline() { Cleanup(); }
MoodbarPipeline::~MoodbarPipeline() {
GstElement *MoodbarPipeline::CreateElement(const QString &factory_name) {
Cleanup();
GstElement *ret = gst_element_factory_make(factory_name.toLatin1().constData(), nullptr);
}
if (ret) {
gst_bin_add(GST_BIN(pipeline_), ret);
GstElement *MoodbarPipeline::CreateElement(const QByteArray &factory_name) {
GstElement *element = gst_element_factory_make(factory_name.constData(), nullptr);
if (element) {
gst_bin_add(GST_BIN(pipeline_), element);
}
else {
qLog(Warning) << "Unable to create gstreamer element" << factory_name;
}
return ret;
return element;
}
@@ -86,6 +90,7 @@ QByteArray MoodbarPipeline::ToGstUrl(const QUrl &url) {
void MoodbarPipeline::Start() {
Q_ASSERT(QThread::currentThread() == thread());
Q_ASSERT(QThread::currentThread() != qApp->thread());
Utilities::SetThreadIOPriority(Utilities::IoPriority::IOPRIO_CLASS_IDLE);
@@ -96,10 +101,10 @@ void MoodbarPipeline::Start() {
pipeline_ = gst_pipeline_new("moodbar-pipeline");
GstElement *decodebin = CreateElement(u"uridecodebin"_s);
convert_element_ = CreateElement(u"audioconvert"_s);
GstElement *spectrum = CreateElement(u"strawberry-fastspectrum"_s);
GstElement *fakesink = CreateElement(u"fakesink"_s);
GstElement *decodebin = CreateElement("uridecodebin");
convert_element_ = CreateElement("audioconvert");
GstElement *spectrum = CreateElement("strawberry-fastspectrum");
GstElement *fakesink = CreateElement("fakesink");
if (!decodebin || !convert_element_ || !spectrum || !fakesink) {
gst_object_unref(GST_OBJECT(pipeline_));
@@ -204,6 +209,8 @@ GstBusSyncReply MoodbarPipeline::BusCallbackSync(GstBus *bus, GstMessage *messag
MoodbarPipeline *instance = reinterpret_cast<MoodbarPipeline*>(self);
if (!instance->running_) return GST_BUS_PASS;
switch (GST_MESSAGE_TYPE(message)) {
case GST_MESSAGE_EOS:
instance->Stop(true);
@@ -224,23 +231,34 @@ GstBusSyncReply MoodbarPipeline::BusCallbackSync(GstBus *bus, GstMessage *messag
void MoodbarPipeline::Stop(const bool success) {
success_ = success;
running_ = false;
QMetaObject::invokeMethod(this, "Finish", Qt::QueuedConnection, Q_ARG(bool, success));
}
void MoodbarPipeline::Finish(const bool success) {
Q_ASSERT(QThread::currentThread() == thread());
Q_ASSERT(QThread::currentThread() != qApp->thread());
success_ = success;
if (builder_) {
data_ = builder_->Finish(1000);
builder_.reset();
}
Cleanup();
Q_EMIT Finished(success);
}
void MoodbarPipeline::Cleanup() {
Q_ASSERT(QThread::currentThread() == thread());
Q_ASSERT(QThread::currentThread() != qApp->thread());
running_ = false;
if (pipeline_) {
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_));
if (bus) {

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2019-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2019-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,6 +26,7 @@
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QSharedPointer>
#include <glib.h>
#include <glib-object.h>
@@ -46,18 +47,18 @@ class MoodbarPipeline : public QObject {
bool success() const { return success_; }
const QByteArray &data() const { return data_; }
public Q_SLOTS:
void Start();
Q_INVOKABLE void Start();
Q_SIGNALS:
void Finished(const bool success);
private:
GstElement *CreateElement(const QString &factory_name);
GstElement *CreateElement(const QByteArray &factory_name);
QByteArray ToGstUrl(const QUrl &url);
void ReportError(GstMessage *msg);
void Stop(const bool success);
Q_INVOKABLE void Finish(const bool success);
void Cleanup();
static void NewPadCallback(GstElement *element, GstPad *pad, gpointer self);
@@ -75,4 +76,6 @@ class MoodbarPipeline : public QObject {
QByteArray data_;
};
using MoodbarPipelinePtr = QSharedPointer<MoodbarPipeline>;
#endif // MOODBARPIPELINE_H

View File

@@ -201,7 +201,7 @@ void MoodbarProxyStyle::drawComplexControl(ComplexControl control, const QStyleO
}
void MoodbarProxyStyle::Render(ComplexControl control, const QStyleOptionSlider *option, QPainter *painter, const QWidget *widget) {
void MoodbarProxyStyle::Render(const ComplexControl control, const QStyleOptionSlider *option, QPainter *painter, const QWidget *widget) {
const qreal fade_value = fade_timeline_->currentValue();

View File

@@ -84,7 +84,7 @@ class MoodbarProxyStyle : public QProxyStyle {
private:
void NextState();
void Render(ComplexControl control, const QStyleOptionSlider *option, QPainter *painter, const QWidget *widget);
void Render(const ComplexControl control, const QStyleOptionSlider *option, QPainter *painter, const QWidget *widget);
void EnsureMoodbarRendered(const QStyleOptionSlider *opt);
void DrawArrow(const QStyleOptionSlider *option, QPainter *painter) const;
void ShowContextMenu(const QPoint pos);
@@ -92,7 +92,7 @@ class MoodbarProxyStyle : public QProxyStyle {
static QPixmap MoodbarPixmap(const ColorVector &colors, const QSize size, const QPalette &palette, const QStyleOptionSlider *opt);
private Q_SLOTS:
void FaderValueChanged(qreal value);
void FaderValueChanged(const qreal value);
void SetStyle(QAction *action);
Q_SIGNALS:

View File

@@ -106,8 +106,7 @@ Mpris2::Mpris2(const SharedPtr<Player> player,
: QObject(parent),
player_(player),
playlist_manager_(playlist_manager),
current_albumcover_loader_(current_albumcover_loader),
app_name_(QCoreApplication::applicationName()) {
current_albumcover_loader_(current_albumcover_loader) {
new Mpris2Root(this);
new Mpris2TrackList(this);
@@ -135,8 +134,6 @@ Mpris2::Mpris2(const SharedPtr<Player> player,
QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistChanged, this, &Mpris2::PlaylistChangedSlot);
QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentChanged, this, &Mpris2::PlaylistCollectionChanged);
app_name_[0] = app_name_[0].toUpper();
QStringList data_dirs = QString::fromUtf8(qgetenv("XDG_DATA_DIRS")).split(u':');
if (!data_dirs.contains("/usr/local/share"_L1)) {
@@ -238,7 +235,7 @@ bool Mpris2::CanRaise() const { return true; }
bool Mpris2::HasTrackList() const { return true; }
QString Mpris2::Identity() const { return app_name_; }
QString Mpris2::Identity() const { return QCoreApplication::applicationName(); }
QString Mpris2::DesktopEntryAbsolutePath() const {
@@ -386,8 +383,12 @@ void Mpris2::SetRating(double rating) {
}
QDBusObjectPath Mpris2::current_track_id() const {
return QDBusObjectPath(QStringLiteral("/org/strawberrymusicplayer/strawberry/Track/%1").arg(QString::number(playlist_manager_->active()->current_row())));
int Mpris2::current_playlist_row() const {
return playlist_manager_->active()->current_row();
}
QDBusObjectPath Mpris2::current_track_id(const int current_row) const {
return QDBusObjectPath(QStringLiteral("/org/strawberrymusicplayer/strawberry/Track/%1").arg(current_row));
}
// We send Metadata change notification as soon as the process of changing song starts...
@@ -405,11 +406,14 @@ void Mpris2::CurrentSongChanged(const Song &song) {
// ... and we add the cover information later, when it's available.
void Mpris2::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) {
const int current_row = current_playlist_row();
if (current_row == -1) return;
last_metadata_ = QVariantMap();
song.ToXesam(&last_metadata_);
using mpris::AddMetadata;
AddMetadata(u"mpris:trackid"_s, current_track_id(), &last_metadata_);
AddMetadata(u"mpris:trackid"_s, current_track_id(current_row), &last_metadata_);
QUrl cover_url;
if (result.album_cover.cover_url.isValid() && result.album_cover.cover_url.isLocalFile() && QFile(result.album_cover.cover_url.toLocalFile()).exists()) {
@@ -519,7 +523,11 @@ void Mpris2::Seek(qint64 offset) {
void Mpris2::SetPosition(const QDBusObjectPath &trackId, qint64 offset) {
if (CanSeek() && trackId == current_track_id() && offset >= 0) {
const int current_row = current_playlist_row();
if (current_row == -1) return;
if (CanSeek() && trackId == current_track_id(current_row) && offset >= 0) {
offset *= kNsecPerUsec;
if (offset < player_->GetCurrentItem()->Metadata().length_nanosec()) {

View File

@@ -230,7 +230,8 @@ class Mpris2 : public QObject {
QString PlaybackStatus(EngineBase::State state) const;
QDBusObjectPath current_track_id() const;
int current_playlist_row() const;
QDBusObjectPath current_track_id(const int current_row) const;
bool CanSeek(EngineBase::State state) const;
@@ -241,7 +242,6 @@ class Mpris2 : public QObject {
const SharedPtr<PlaylistManager> playlist_manager_;
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader_;
QString app_name_;
QString desktopfilepath_;
QVariantMap last_metadata_;

View File

@@ -292,7 +292,7 @@ void Organize::ProcessSomeFiles() {
}
Song::FileType Organize::CheckTranscode(Song::FileType original_type) const {
Song::FileType Organize::CheckTranscode(const Song::FileType original_type) const {
if (original_type == Song::FileType::Stream) return Song::FileType::Unknown;
@@ -319,7 +319,7 @@ Song::FileType Organize::CheckTranscode(Song::FileType original_type) const {
}
void Organize::SetSongProgress(float progress, bool transcoded) {
void Organize::SetSongProgress(const float progress, const bool transcoded) {
const int max = transcoded ? 50 : 100;
current_copy_progress_ = (transcoded ? 50 : 0) + qBound(0, static_cast<int>(progress * static_cast<float>(max)), max - 1);
@@ -359,7 +359,7 @@ void Organize::UpdateProgress() {
}
void Organize::FileTranscoded(const QString &input, const QString &output, bool success) {
void Organize::FileTranscoded(const QString &input, const QString &output, const bool success) {
Q_UNUSED(output);

View File

@@ -87,13 +87,13 @@ class Organize : public QObject {
private Q_SLOTS:
void ProcessSomeFiles();
void FileTranscoded(const QString &input, const QString &output, bool success);
void FileTranscoded(const QString &input, const QString &output, const bool success);
void LogLine(const QString &message);
private:
void SetSongProgress(float progress, bool transcoded = false);
void SetSongProgress(const float progress, const bool transcoded = false);
void UpdateProgress();
Song::FileType CheckTranscode(Song::FileType original_type) const;
Song::FileType CheckTranscode(const Song::FileType original_type) const;
private:
struct Task {

View File

@@ -411,7 +411,6 @@ void OrganizeDialog::SetLoadingSongs(const bool loading) {
SongList OrganizeDialog::LoadSongsBlocking(const QStringList &filenames) const {
SongList songs;
Song song;
QStringList filenames_copy = filenames;
while (!filenames_copy.isEmpty()) {
@@ -427,6 +426,7 @@ SongList OrganizeDialog::LoadSongsBlocking(const QStringList &filenames) const {
continue;
}
Song song;
const TagReaderResult result = tagreader_client_->ReadFileBlocking(filename, &song);
if (result.success() && song.is_valid()) {
songs << song;
@@ -476,6 +476,7 @@ Organize::NewSongInfoList OrganizeDialog::ComputeNewSongsFilenames(const SongLis
}
new_songs_info << Organize::NewSongInfo(song, result.filename, result.unique_filename);
}
return new_songs_info;
}

View File

@@ -48,7 +48,6 @@ OSDBase::OSDBase(const SharedPtr<SystemTrayIcon> tray_icon, QObject *parent)
: QObject(parent),
tray_icon_(tray_icon),
pretty_popup_(new OSDPretty(OSDPretty::Mode::Popup)),
app_name_(QCoreApplication::applicationName()),
timeout_msec_(5000),
type_(OSDSettings::Type::Native),
show_on_volume_change_(false),
@@ -59,11 +58,7 @@ OSDBase::OSDBase(const SharedPtr<SystemTrayIcon> tray_icon, QObject *parent)
use_custom_text_(false),
force_show_next_(false),
ignore_next_stopped_(false),
playing_(false) {
app_name_[0] = app_name_[0].toUpper();
}
playing_(false) {}
OSDBase::~OSDBase() {
delete pretty_popup_;
@@ -264,7 +259,7 @@ void OSDBase::Stopped() {
}
void OSDBase::StopAfterToggle(const bool stop) {
ShowMessage(app_name_, tr("Stop playing after track: %1").arg(stop ? tr("On") : tr("Off")));
ShowMessage(QCoreApplication::applicationName(), tr("Stop playing after track: %1").arg(stop ? tr("On") : tr("Off")));
}
void OSDBase::PlaylistFinished() {
@@ -272,7 +267,7 @@ void OSDBase::PlaylistFinished() {
// We get a PlaylistFinished followed by a Stopped from the player
ignore_next_stopped_ = true;
ShowMessage(app_name_, tr("Playlist finished"));
ShowMessage(QCoreApplication::applicationName(), tr("Playlist finished"));
}
@@ -290,7 +285,7 @@ void OSDBase::VolumeChanged(const uint value) {
}
#endif
ShowMessage(app_name_, message);
ShowMessage(QCoreApplication::applicationName(), message);
}
@@ -346,7 +341,7 @@ void OSDBase::ShuffleModeChanged(const PlaylistSequence::ShuffleMode mode) {
case PlaylistSequence::ShuffleMode::InsideAlbum: current_mode = tr("Shuffle tracks in this album"); break;
case PlaylistSequence::ShuffleMode::Albums: current_mode = tr("Shuffle albums"); break;
}
ShowMessage(app_name_, current_mode);
ShowMessage(QCoreApplication::applicationName(), current_mode);
}
}
@@ -363,7 +358,7 @@ void OSDBase::RepeatModeChanged(const PlaylistSequence::RepeatMode mode) {
case PlaylistSequence::RepeatMode::OneByOne: current_mode = tr("Stop after every track"); break;
case PlaylistSequence::RepeatMode::Intro: current_mode = tr("Intro tracks"); break;
}
ShowMessage(app_name_, current_mode);
ShowMessage(QCoreApplication::applicationName(), current_mode);
}
}

View File

@@ -53,8 +53,6 @@ class OSDBase : public QObject {
virtual bool SupportsNativeNotifications();
virtual bool SupportsTrayPopups();
QString app_name() { return app_name_; }
public Q_SLOTS:
void ReloadSettings();
@@ -88,7 +86,6 @@ class OSDBase : public QObject {
const SharedPtr<SystemTrayIcon> tray_icon_;
OSDPretty *pretty_popup_;
QString app_name_;
int timeout_msec_;
OSDSettings::Type type_;
bool show_on_volume_change_;

View File

@@ -170,7 +170,7 @@ void OSDDBus::ShowMessageNative(const QString &summary, const QString &message,
id = notification_id_;
}
QDBusPendingReply<uint> reply = interface_->Notify(app_name(), id, icon, summary_stripped, message, QStringList(), hints, timeout_msec());
QDBusPendingReply<uint> reply = interface_->Notify(QCoreApplication::applicationName(), id, icon, summary_stripped, message, QStringList(), hints, timeout_msec());
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &OSDDBus::CallFinished);

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -313,8 +313,8 @@ QVariant Playlist::data(const QModelIndex &idx, const int role) const {
case Qt::EditRole:
case Qt::ToolTipRole:
case Qt::DisplayRole:{
PlaylistItemPtr item = items_[idx.row()];
Song song = item->Metadata();
const PlaylistItemPtr item = items_[idx.row()];
const Song song = item->Metadata();
// Don't forget to change Playlist::CompareItems when adding new columns
switch (static_cast<Column>(idx.column())) {
@@ -423,8 +423,8 @@ bool Playlist::setData(const QModelIndex &idx, const QVariant &value, const int
Q_UNUSED(role);
int row = idx.row();
PlaylistItemPtr item = item_at(row);
const int row = idx.row();
const PlaylistItemPtr item = item_at(row);
Song song = item->OriginalMetadata();
if (idx.data() == value) return false;
@@ -487,7 +487,7 @@ void Playlist::ItemReload(const QPersistentModelIndex &idx, const Song &old_meta
void Playlist::ItemReloadComplete(const QPersistentModelIndex &idx, const Song &old_metadata, const bool metadata_edit) {
if (idx.isValid()) {
PlaylistItemPtr item = item_at(idx.row());
const PlaylistItemPtr item = item_at(idx.row());
if (item) {
ItemChanged(idx.row(), ChangedColumns(old_metadata, item->Metadata()));
if (idx.row() == current_row()) {
@@ -555,12 +555,12 @@ int Playlist::NextVirtualIndex(int i, const bool ignore_repeat_track) const {
}
// We need to advance i until we get something else on the same album
Song last_song = current_item_metadata();
const Song last_song = current_item_metadata();
for (int j = i + 1; j < virtual_items_.count(); ++j) {
if (item_at(virtual_items_[j])->GetShouldSkip()) {
continue;
}
Song this_song = item_at(virtual_items_[j])->Metadata();
const Song this_song = item_at(virtual_items_[j])->Metadata();
if (((last_song.is_compilation() && this_song.is_compilation()) ||
last_song.effective_albumartist() == this_song.effective_albumartist()) &&
last_song.album() == this_song.album() &&
@@ -677,13 +677,13 @@ int Playlist::previous_row(const bool ignore_repeat_track) {
void Playlist::set_current_row(const int i, const AutoScroll autoscroll, const bool is_stopping, const bool force_inform) {
QPersistentModelIndex old_current_item_index = current_item_index_;
const QPersistentModelIndex old_current_item_index = current_item_index_;
QPersistentModelIndex new_current_item_index;
if (i != -1) new_current_item_index = QPersistentModelIndex(index(i, 0, QModelIndex()));
if (new_current_item_index != current_item_index_) ClearStreamMetadata();
int nextrow = next_row();
const int nextrow = next_row();
if (nextrow != -1 && nextrow != i) {
PlaylistItemPtr next_item = item_at(nextrow);
if (next_item) {
@@ -1127,7 +1127,7 @@ void Playlist::InsertItemsWithoutUndo(const PlaylistItemPtrList &items, const in
beginInsertRows(QModelIndex(), start, end);
for (int i = start; i <= end; ++i) {
PlaylistItemPtr item = items[i - start];
const PlaylistItemPtr item = items[i - start];
items_.insert(i, item);
virtual_items_ << static_cast<int>(virtual_items_.count());
@@ -1180,7 +1180,11 @@ void Playlist::InsertSongs(const SongList &songs, const int pos, const bool play
InsertSongItems<SongPlaylistItem>(songs, pos, play_now, enqueue, enqueue_next);
}
void Playlist::InsertSongsOrCollectionItems(const SongList &songs, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
void Playlist::InsertSongsOrCollectionItems(const SongList &songs, const QString &playlist_name, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
if (!playlist_name.isEmpty()) {
Q_EMIT Rename(id_, playlist_name);
}
PlaylistItemPtrList items;
for (const Song &song : songs) {
@@ -1201,6 +1205,7 @@ void Playlist::InsertSongsOrCollectionItems(const SongList &songs, const int pos
}
}
}
InsertItems(items, pos, play_now, enqueue, enqueue_next);
}
@@ -1304,11 +1309,11 @@ QMimeData *Playlist::mimeData(const QModelIndexList &indexes) const {
rows << idx.row();
}
QBuffer buf;
if (!buf.open(QIODevice::WriteOnly)) {
QBuffer buffer;
if (!buffer.open(QIODevice::WriteOnly)) {
return nullptr;
}
QDataStream stream(&buf);
QDataStream stream(&buffer);
const Playlist *self = this;
const qint64 pid = QCoreApplication::applicationPid();
@@ -1316,11 +1321,11 @@ QMimeData *Playlist::mimeData(const QModelIndexList &indexes) const {
stream.writeRawData(reinterpret_cast<char*>(&self), sizeof(self)); // NOLINT(bugprone-sizeof-expression)
stream << rows;
stream.writeRawData(reinterpret_cast<const char*>(&pid), sizeof(pid));
buf.close();
buffer.close();
QMimeData *mimedata = new QMimeData;
mimedata->setUrls(urls);
mimedata->setData(QLatin1String(kRowsMimetype), buf.data());
mimedata->setData(QLatin1String(kRowsMimetype), buffer.data());
return mimedata;
@@ -1607,21 +1612,21 @@ void Playlist::ItemsLoaded() {
InsertItems(items, 0);
is_loading_ = false;
PlaylistBackend::Playlist p = playlist_backend_->GetPlaylist(id_);
const PlaylistBackend::Playlist playlist = playlist_backend_->GetPlaylist(id_);
// The newly loaded list of items might be shorter than it was before so look out for a bad last_played index
last_played_item_index_ = p.last_played == -1 || p.last_played >= rowCount() ? QModelIndex() : index(p.last_played);
last_played_item_index_ = playlist.last_played == -1 || playlist.last_played >= rowCount() ? QModelIndex() : index(playlist.last_played);
if (p.dynamic_type == PlaylistGenerator::Type::Query) {
PlaylistGeneratorPtr gen = PlaylistGenerator::Create(p.dynamic_type);
if (playlist.dynamic_type == PlaylistGenerator::Type::Query) {
PlaylistGeneratorPtr gen = PlaylistGenerator::Create(playlist.dynamic_type);
if (gen) {
SharedPtr<CollectionBackend> backend = nullptr;
if (p.dynamic_backend == collection_backend_->songs_table()) backend = collection_backend_;
if (playlist.dynamic_backend == collection_backend_->songs_table()) backend = collection_backend_;
if (backend) {
gen->set_collection_backend(collection_backend_);
gen->Load(p.dynamic_data);
gen->Load(playlist.dynamic_data);
TurnOnDynamicPlaylist(gen);
}
@@ -1637,7 +1642,7 @@ void Playlist::ItemsLoaded() {
// Should we gray out deleted songs asynchronously on startup?
if (greyout) {
(void)QtConcurrent::run(&Playlist::InvalidateDeletedSongs, this);
InvalidateDeletedSongs();
}
Q_EMIT PlaylistLoaded();
@@ -1725,11 +1730,11 @@ PlaylistItemPtrList Playlist::RemoveItemsWithoutUndo(const int row, const int co
// Remove items
beginRemoveRows(QModelIndex(), row, row + count - 1);
PlaylistItemPtrList ret;
ret.reserve(count);
PlaylistItemPtrList items;
items.reserve(count);
for (int i = 0; i < count; ++i) {
PlaylistItemPtr item(items_.takeAt(row));
ret << item;
items << item;
if (item->source() == Song::Source::Collection) {
int id = item->Metadata().id();
@@ -1769,13 +1774,13 @@ PlaylistItemPtrList Playlist::RemoveItemsWithoutUndo(const int row, const int co
ScheduleSave();
return ret;
return items;
}
void Playlist::StopAfter(const int row) {
QModelIndex old_stop_after = stop_after_;
const QModelIndex old_stop_after = stop_after_;
if ((stop_after_.isValid() && stop_after_.row() == row) || row == -1) {
stop_after_ = QModelIndex();
@@ -1911,9 +1916,9 @@ void Playlist::RemoveItemsNotInQueue() {
void Playlist::ReloadItems(const QList<int> &rows) {
for (int row : rows) {
PlaylistItemPtr item = item_at(row);
QPersistentModelIndex idx = index(row, 0);
for (const int row : rows) {
const PlaylistItemPtr item = item_at(row);
const QPersistentModelIndex idx = index(row, 0);
if (idx.isValid()) {
ItemReload(idx, item->Metadata(), false);
}
@@ -1921,18 +1926,6 @@ void Playlist::ReloadItems(const QList<int> &rows) {
}
void Playlist::ReloadItemsBlocking(const QList<int> &rows) {
for (int row : rows) {
PlaylistItemPtr item = item_at(row);
Song old_metadata = item->Metadata();
item->Reload();
QPersistentModelIndex idx = index(row, 0);
ItemReloadComplete(idx, old_metadata, false);
}
}
void Playlist::Shuffle() {
PlaylistItemPtrList new_items(items_);
@@ -1951,7 +1944,7 @@ void Playlist::Shuffle() {
const int count = static_cast<int>(items_.count());
for (int i = begin; i < count; ++i) {
int new_pos = i + (rand() % (count - i));
const int new_pos = i + (rand() % (count - i));
std::swap(new_items[i], new_items[new_pos]);
}
@@ -2053,12 +2046,12 @@ PlaylistFilter *Playlist::filter() const { return filter_; }
SongList Playlist::GetAllSongs() const {
SongList ret;
ret.reserve(items_.count());
SongList songs;
songs.reserve(items_.count());
for (PlaylistItemPtr item : items_) { // clazy:exclude=range-loop-reference
ret << item->Metadata();
songs << item->Metadata();
}
return ret;
return songs;
}
@@ -2066,12 +2059,13 @@ PlaylistItemPtrList Playlist::GetAllItems() const { return items_; }
quint64 Playlist::GetTotalLength() const {
quint64 ret = 0;
quint64 total_length = 0;
for (PlaylistItemPtr item : items_) { // clazy:exclude=range-loop-reference
qint64 length = item->Metadata().length_nanosec();
if (length > 0) ret += length;
if (length > 0) total_length += length;
}
return ret;
return total_length;
}
@@ -2112,7 +2106,7 @@ void Playlist::TracksEnqueued(const QModelIndex &parent_idx, const int begin, co
void Playlist::QueueLayoutChanged() {
for (int i = 0; i < queue_->rowCount(); ++i) {
const QModelIndex &idx = queue_->mapToSource(queue_->index(i, static_cast<int>(Column::Title)));
const QModelIndex idx = queue_->mapToSource(queue_->index(i, static_cast<int>(Column::Title)));
Q_EMIT dataChanged(idx, idx);
}
@@ -2309,10 +2303,10 @@ void Playlist::InvalidateDeletedSongs() {
for (int row = 0; row < items_.count(); ++row) {
PlaylistItemPtr item = items_.value(row);
Song song = item->Metadata();
const Song song = item->Metadata();
if (song.url().isLocalFile()) {
bool exists = QFile::exists(song.url().toLocalFile());
if (song.url().isValid() && song.url().isLocalFile()) {
const bool exists = QFile::exists(song.url().toLocalFile());
if (!exists && !item->HasForegroundColor(kInvalidSongPriority)) {
// Gray out the song if it's not there
@@ -2327,12 +2321,7 @@ void Playlist::InvalidateDeletedSongs() {
}
if (!invalidated_rows.isEmpty()) {
if (QThread::currentThread() == thread()) {
ReloadItems(invalidated_rows);
}
else {
ReloadItemsBlocking(invalidated_rows);
}
ReloadItems(invalidated_rows);
}
}
@@ -2342,8 +2331,8 @@ void Playlist::RemoveDeletedSongs() {
QList<int> rows_to_remove;
for (int row = 0; row < items_.count(); ++row) {
PlaylistItemPtr item = items_.value(row);
Song song = item->Metadata();
const PlaylistItemPtr item = items_.value(row);
const Song song = item->Metadata();
if (song.url().isLocalFile() && !QFile::exists(song.url().toLocalFile())) {
rows_to_remove.append(row); // clazy:exclude=reserve-candidates
@@ -2376,7 +2365,7 @@ void Playlist::RemoveDuplicateSongs() {
std::unordered_map<Song, int, SongSimilarHash, SongSimilarEqual> unique_songs;
for (int row = 0; row < items_.count(); ++row) {
PlaylistItemPtr item = items_.value(row);
const PlaylistItemPtr item = items_.value(row);
const Song &song = item->Metadata();
bool found_duplicate = false;
@@ -2409,7 +2398,7 @@ void Playlist::RemoveUnavailableSongs() {
QList<int> rows_to_remove;
for (int row = 0; row < items_.count(); ++row) {
PlaylistItemPtr item = items_.value(row);
const PlaylistItemPtr item = items_.value(row);
const Song &song = item->Metadata();
// Check only local files
@@ -2424,10 +2413,10 @@ void Playlist::RemoveUnavailableSongs() {
bool Playlist::ApplyValidityOnCurrentSong(const QUrl &url, const bool valid) {
PlaylistItemPtr current = current_item();
const PlaylistItemPtr current = current_item();
if (current) {
Song current_song = current->Metadata();
const Song current_song = current->Metadata();
// If validity has changed, reload the item
if (current_song.source() == Song::Source::LocalFile || current_song.source() == Song::Source::Collection) {
@@ -2523,7 +2512,7 @@ void Playlist::TurnOffDynamicPlaylist() {
void Playlist::RateSong(const QModelIndex &idx, const float rating) {
if (has_item_at(idx.row())) {
PlaylistItemPtr item = item_at(idx.row());
const PlaylistItemPtr item = item_at(idx.row());
if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
collection_backend_->UpdateSongRatingAsync(item->Metadata().id(), rating);
}
@@ -2537,7 +2526,7 @@ void Playlist::RateSongs(const QModelIndexList &index_list, const float rating)
for (const QModelIndex &idx : index_list) {
const int row = idx.row();
if (has_item_at(row)) {
PlaylistItemPtr item = item_at(row);
const PlaylistItemPtr item = item_at(row);
if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
id_list << item->Metadata().id(); // clazy:exclude=reserve-candidates
}

View File

@@ -226,7 +226,7 @@ class Playlist : public QAbstractListModel {
void InsertItems(const PlaylistItemPtrList &itemsIn, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertCollectionItems(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertSongs(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertSongsOrCollectionItems(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertSongsOrCollectionItems(const SongList &songs, const QString &playlist_name = QString(), const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertSmartPlaylist(PlaylistGeneratorPtr gen, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertStreamingItems(StreamingServicePtr service, const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
void InsertRadioItems(const SongList &songs, const int pos = -1, const bool play_now = false, const bool enqueue = false, const bool enqueue_next = false);
@@ -239,14 +239,12 @@ class Playlist : public QAbstractListModel {
// song will be reloaded to even out the situation because obviously something has changed.
// This returns true if this playlist had current item when the method was invoked.
bool ApplyValidityOnCurrentSong(const QUrl &url, bool valid);
// Grays out and reloads all deleted songs in all playlists. Also, "ungreys" those songs which were once deleted but now got restored somehow.
void InvalidateDeletedSongs();
// Removes from the playlist all local files that don't exist anymore.
void RemoveDeletedSongs();
void StopAfter(const int row);
void ReloadItems(const QList<int> &rows);
void ReloadItemsBlocking(const QList<int> &rows);
void InformOfCurrentSongChange(const bool minor);
// Just emits the dataChanged() signal so the mood column is repainted.
@@ -329,6 +327,8 @@ class Playlist : public QAbstractListModel {
// Signals that the queue has changed, meaning that the remaining queued items should update their position.
void QueueChanged();
void Rename(const int id, const QString &name);
private:
void SetCurrentIsPaused(const bool paused);
int NextVirtualIndex(int i, const bool ignore_repeat_track) const;
@@ -354,6 +354,9 @@ class Playlist : public QAbstractListModel {
void TurnOnDynamicPlaylist(PlaylistGeneratorPtr gen);
void InsertDynamicItems(const int count);
// Grays out and reloads all deleted songs in all playlists. Also, "ungreys" those songs which were once deleted but now got restored somehow.
void InvalidateDeletedSongs();
private Q_SLOTS:
void TracksAboutToBeDequeued(const QModelIndex&, const int begin, const int end);
void TracksDequeued();

View File

@@ -298,7 +298,7 @@ PlaylistItemPtr PlaylistBackend::RestoreCueData(PlaylistItemPtr item, SharedPtr<
return item;
}
SongList song_list;
SongList songs;
{
QMutexLocker locker(&state->mutex_);
@@ -306,16 +306,16 @@ PlaylistItemPtr PlaylistBackend::RestoreCueData(PlaylistItemPtr item, SharedPtr<
QFile cue_file(cue_path);
if (!cue_file.open(QIODevice::ReadOnly)) return item;
song_list = cue_parser.Load(&cue_file, cue_path, QDir(cue_path.section(u'/', 0, -2)));
songs = cue_parser.Load(&cue_file, cue_path, QDir(cue_path.section(u'/', 0, -2))).songs;
cue_file.close();
state->cached_cues_[cue_path] = song_list;
state->cached_cues_[cue_path] = songs;
}
else {
song_list = state->cached_cues_[cue_path];
songs = state->cached_cues_[cue_path];
}
}
for (const Song &from_list : std::as_const(song_list)) {
for (const Song &from_list : std::as_const(songs)) {
if (from_list.url().toEncoded() == song.url().toEncoded() && from_list.beginning_nanosec() == song.beginning_nanosec()) {
// We found a matching section; replace the input item with a new one containing CUE metadata
return make_shared<SongPlaylistItem>(from_list);

View File

@@ -166,6 +166,7 @@ Playlist *PlaylistManager::AddPlaylist(const int id, const QString &name, const
QObject::connect(ret, &Playlist::EditingFinished, this, &PlaylistManager::EditingFinished);
QObject::connect(ret, &Playlist::Error, this, &PlaylistManager::Error);
QObject::connect(ret, &Playlist::PlayRequested, this, &PlaylistManager::PlayRequested);
QObject::connect(ret, &Playlist::Rename, this, &PlaylistManager::Rename);
QObject::connect(playlist_container_->view(), &PlaylistView::ColumnAlignmentChanged, ret, &Playlist::SetColumnAlignment);
QObject::connect(&*current_albumcover_loader_, &CurrentAlbumCoverLoader::AlbumCoverLoaded, ret, &Playlist::AlbumCoverLoaded);
@@ -208,7 +209,7 @@ void PlaylistManager::Load(const QString &filename) {
QFileInfo fileinfo(filename);
int id = playlist_backend_->CreatePlaylist(fileinfo.completeBaseName(), QString());
const int id = playlist_backend_->CreatePlaylist(fileinfo.completeBaseName(), QString());
if (id == -1) {
Q_EMIT Error(tr("Couldn't create playlist"));
@@ -221,17 +222,17 @@ void PlaylistManager::Load(const QString &filename) {
}
void PlaylistManager::Save(const int id, const QString &filename, const PlaylistSettings::PathType path_type) {
void PlaylistManager::Save(const int id, const QString &playlist_name, const QString &filename, const PlaylistSettings::PathType path_type) {
if (playlists_.contains(id)) {
parser_->Save(playlist(id)->GetAllSongs(), filename, path_type);
parser_->Save(playlist_name, playlist(id)->GetAllSongs(), filename, path_type);
}
else {
// Playlist is not in the playlist manager: probably save action was triggered from the left sidebar and the playlist isn't loaded.
QFuture<SongList> future = QtConcurrent::run(&PlaylistBackend::GetPlaylistSongs, playlist_backend_, id);
QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [this, watcher, filename, path_type]() {
ItemsLoadedForSavePlaylist(watcher->result(), filename, path_type);
QObject::connect(watcher, &QFutureWatcher<SongList>::finished, this, [this, watcher, playlist_name, filename, path_type]() {
ItemsLoadedForSavePlaylist(playlist_name, watcher->result(), filename, path_type);
watcher->deleteLater();
});
watcher->setFuture(future);
@@ -239,9 +240,9 @@ void PlaylistManager::Save(const int id, const QString &filename, const Playlist
}
void PlaylistManager::ItemsLoadedForSavePlaylist(const SongList &songs, const QString &filename, const PlaylistSettings::PathType path_type) {
void PlaylistManager::ItemsLoadedForSavePlaylist(const QString &playlist_name, const SongList &songs, const QString &filename, const PlaylistSettings::PathType path_type) {
parser_->Save(songs, filename, path_type);
parser_->Save(playlist_name, songs, filename, path_type);
}
@@ -255,7 +256,7 @@ void PlaylistManager::SaveWithUI(const int id, const QString &playlist_name) {
s.endGroup();
QString suggested_filename = playlist_name;
QString filename = last_save_path + QLatin1Char('/') + suggested_filename.remove(QRegularExpression(QLatin1String(kProblematicCharactersRegex), QRegularExpression::CaseInsensitiveOption)) + QLatin1Char('.') + last_save_extension;
QString filename = last_save_path + QLatin1Char('/') + suggested_filename.remove(u'/').remove(QRegularExpression(QLatin1String(kProblematicCharactersRegex), QRegularExpression::CaseInsensitiveOption)) + QLatin1Char('.') + last_save_extension;
QFileInfo fileinfo;
Q_FOREVER {
@@ -283,7 +284,7 @@ void PlaylistManager::SaveWithUI(const int id, const QString &playlist_name) {
s.setValue(PlaylistSettings::kLastSaveExtension, fileinfo.suffix());
s.endGroup();
Save(id == -1 ? current_id() : id, filename, path_type);
Save(id == -1 ? current_id() : id, playlist_name, filename, path_type);
}
@@ -525,15 +526,6 @@ void PlaylistManager::RemoveCurrentSong() const {
active()->removeRows(active()->current_index().row(), 1);
}
void PlaylistManager::InvalidateDeletedSongs() {
const QList<Playlist*> playlists = GetAllPlaylists();
for (Playlist *playlist : playlists) {
playlist->InvalidateDeletedSongs();
}
}
void PlaylistManager::RemoveDeletedSongs() {
const QList<Playlist*> playlists = GetAllPlaylists();
@@ -618,7 +610,7 @@ void PlaylistManager::SaveAllPlaylists() {
for (QMap<int, Data>::const_iterator it = playlists_.constBegin(); it != playlists_.constEnd(); ++it) {
const Data &data = *it;
const QString filepath = path + QLatin1Char('/') + data.name + QLatin1Char('.') + extension;
Save(it.key(), filepath, path_type);
Save(it.key(), data.name, filepath, path_type);
}
}

View File

@@ -74,8 +74,6 @@ class PlaylistManager : public PlaylistManagerInterface {
// Returns the collection of playlists managed by this PlaylistManager.
QList<Playlist*> GetAllPlaylists() const override;
// Grays out and reloads all deleted songs in all playlists.
void InvalidateDeletedSongs() override;
// Removes all deleted songs from all playlists.
void RemoveDeletedSongs() override;
// Returns true if the playlist is open
@@ -99,7 +97,7 @@ class PlaylistManager : public PlaylistManagerInterface {
public Q_SLOTS:
void New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()) override;
void Load(const QString &filename) override;
void Save(const int id, const QString &filename, const PlaylistSettings::PathType path_type) override;
void Save(const int id, const QString &playlist_name, const QString &filename, const PlaylistSettings::PathType path_type) override;
// Display a file dialog to let user choose a file before saving the file
void SaveWithUI(const int id, const QString &playlist_name);
void Rename(const int id, const QString &new_name) override;
@@ -150,7 +148,7 @@ class PlaylistManager : public PlaylistManagerInterface {
void OneOfPlaylistsChanged();
void UpdateSummaryText();
void UpdateCollectionSongs(const SongList &songs);
void ItemsLoadedForSavePlaylist(const SongList &songs, const QString &filename, const PlaylistSettings::PathType path_type);
void ItemsLoadedForSavePlaylist(const QString &playlist_name, const SongList &songs, const QString &filename, const PlaylistSettings::PathType path_type);
void PlaylistLoaded();
private:

View File

@@ -59,8 +59,6 @@ class PlaylistManagerInterface : public QObject {
// Returns the collection of playlists managed by this PlaylistManager.
virtual QList<Playlist*> GetAllPlaylists() const = 0;
// Grays out and reloads all deleted songs in all playlists.
virtual void InvalidateDeletedSongs() = 0;
// Removes all deleted songs from all playlists.
virtual void RemoveDeletedSongs() = 0;
@@ -81,7 +79,7 @@ class PlaylistManagerInterface : public QObject {
public Q_SLOTS:
virtual void New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()) = 0;
virtual void Load(const QString &filename) = 0;
virtual void Save(const int id, const QString &filename, const PlaylistSettings::PathType path_type) = 0;
virtual void Save(const int id, const QString &playlist_name, const QString &filename, const PlaylistSettings::PathType path_type) = 0;
virtual void Rename(const int id, const QString &new_name) = 0;
virtual void Delete(const int id) = 0;
virtual bool Close(const int id) = 0;

View File

@@ -54,7 +54,7 @@ SongLoaderInserter::SongLoaderInserter(const SharedPtr<TaskManager> task_manager
SongLoaderInserter::~SongLoaderInserter() { qDeleteAll(pending_); }
void SongLoaderInserter::Load(Playlist *destination, int row, bool play_now, bool enqueue, bool enqueue_next, const QList<QUrl> &urls) {
void SongLoaderInserter::Load(Playlist *destination, const int row, const bool play_now, const bool enqueue, const bool enqueue_next, const QList<QUrl> &urls) {
destination_ = destination;
row_ = row;
@@ -69,15 +69,16 @@ void SongLoaderInserter::Load(Playlist *destination, int row, bool play_now, boo
for (const QUrl &url : urls) {
SongLoader *loader = new SongLoader(url_handlers_, collection_backend_, tagreader_client_, this);
SongLoader::Result ret = loader->Load(url);
const SongLoader::Result result = loader->Load(url);
if (ret == SongLoader::Result::BlockingLoadRequired) {
if (result == SongLoader::Result::BlockingLoadRequired) {
pending_.append(loader);
continue;
}
if (ret == SongLoader::Result::Success) {
if (result == SongLoader::Result::Success) {
songs_ << loader->songs();
playlist_name_ = loader->playlist_name();
}
else {
const QStringList errors = loader->errors();
@@ -101,7 +102,7 @@ void SongLoaderInserter::Load(Playlist *destination, int row, bool play_now, boo
// First, we add tracks (without metadata) into the playlist
// In the meantime, MusicBrainz will be queried to get songs' metadata.
// AudioCDTagsLoaded will be called next, and playlist's items will be updated.
void SongLoaderInserter::LoadAudioCD(Playlist *destination, int row, bool play_now, bool enqueue, bool enqueue_next) {
void SongLoaderInserter::LoadAudioCD(Playlist *destination, const int row, const bool play_now, const bool enqueue, const bool enqueue_next) {
destination_ = destination;
row_ = row;
@@ -113,8 +114,8 @@ void SongLoaderInserter::LoadAudioCD(Playlist *destination, int row, bool play_n
QObject::connect(loader, &SongLoader::AudioCDTracksLoadFinished, this, [this, loader]() { AudioCDTracksLoadFinished(loader); });
QObject::connect(loader, &SongLoader::LoadAudioCDFinished, this, &SongLoaderInserter::AudioCDTagsLoaded);
qLog(Info) << "Loading audio CD...";
SongLoader::Result ret = loader->LoadAudioCD();
if (ret == SongLoader::Result::Error) {
const SongLoader::Result result = loader->LoadAudioCD();
if (result == SongLoader::Result::Error) {
if (loader->errors().isEmpty())
Q_EMIT Error(tr("Error while loading audio CD."));
else {
@@ -166,7 +167,7 @@ void SongLoaderInserter::InsertSongs() {
// Insert songs (that haven't been completely loaded) to allow user to see and play them while not loaded completely
if (destination_) {
destination_->InsertSongsOrCollectionItems(songs_, row_, play_now_, enqueue_, enqueue_next_);
destination_->InsertSongsOrCollectionItems(songs_, playlist_name_, row_, play_now_, enqueue_, enqueue_next_);
}
}
@@ -180,10 +181,10 @@ void SongLoaderInserter::AsyncLoad() {
bool first_loaded = false;
for (int i = 0; i < pending_.count(); ++i) {
SongLoader *loader = pending_.value(i);
SongLoader::Result res = loader->LoadFilenamesBlocking();
const SongLoader::Result result = loader->LoadFilenamesBlocking();
task_manager_->SetTaskProgress(async_load_id, ++async_progress);
if (res == SongLoader::Result::Error) {
if (result == SongLoader::Result::Error) {
const QStringList errors = loader->errors();
for (const QString &error : errors) {
Q_EMIT Error(error);
@@ -199,6 +200,7 @@ void SongLoaderInserter::AsyncLoad() {
}
songs_ << loader->songs();
playlist_name_ = loader->playlist_name();
}
task_manager_->SetTaskFinished(async_load_id);

View File

@@ -52,8 +52,8 @@ class SongLoaderInserter : public QObject {
~SongLoaderInserter() override;
void Load(Playlist *destination, int row, bool play_now, bool enqueue, bool enqueue_next, const QList<QUrl> &urls);
void LoadAudioCD(Playlist *destination, int row, bool play_now, bool enqueue, bool enqueue_next);
void Load(Playlist *destination, const int row, const bool play_now, const bool enqueue, const bool enqueue_next, const QList<QUrl> &urls);
void LoadAudioCD(Playlist *destination, const int row, const bool play_now, const bool enqueue, const bool enqueue_next);
Q_SIGNALS:
void Error(const QString &message);
@@ -82,6 +82,7 @@ class SongLoaderInserter : public QObject {
bool enqueue_next_;
SongList songs_;
QString playlist_name_;
QList<SongLoader*> pending_;
};

View File

@@ -42,7 +42,7 @@ bool AsxIniParser::TryMagic(const QByteArray &data) const {
return data.toLower().contains("[reference]");
}
SongList AsxIniParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
ParserBase::LoadResult AsxIniParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
Q_UNUSED(playlist_path);
@@ -66,7 +66,9 @@ SongList AsxIniParser::Load(QIODevice *device, const QString &playlist_path, con
}
void AsxIniParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
void AsxIniParser::Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
Q_UNUSED(playlist_name)
QTextStream s(device);
s << "[Reference]" << Qt::endl;

View File

@@ -51,8 +51,8 @@ class AsxIniParser : public ParserBase {
bool TryMagic(const QByteArray &data) const override;
SongList Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
LoadResult Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const override;
void Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
};
#endif // ASXINIPARSER_H

View File

@@ -42,7 +42,7 @@ class CollectionBackendInterface;
ASXParser::ASXParser(const SharedPtr<TagReaderClient> tagreader_client, const SharedPtr<CollectionBackendInterface> collection_backend, QObject *parent)
: XMLParser(tagreader_client, collection_backend, parent) {}
SongList ASXParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
ParserBase::LoadResult ASXParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
Q_UNUSED(playlist_path);
@@ -133,8 +133,9 @@ return_song:
}
void ASXParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
void ASXParser::Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
Q_UNUSED(playlist_name)
Q_UNUSED(dir)
Q_UNUSED(path_type)

View File

@@ -53,8 +53,8 @@ class ASXParser : public XMLParser {
bool TryMagic(const QByteArray &data) const override;
SongList Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
LoadResult Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const override;
void Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
private:
Song ParseTrack(QXmlStreamReader *reader, const QDir &dir, const bool collection_lookup) const;

View File

@@ -66,7 +66,7 @@ constexpr char kDisc[] = "discnumber";
CueParser::CueParser(const SharedPtr<TagReaderClient> tagreader_client, const SharedPtr<CollectionBackendInterface> collection_backend, QObject *parent)
: ParserBase(tagreader_client, collection_backend, parent) {}
SongList CueParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
ParserBase::LoadResult CueParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
SongList ret;
@@ -378,8 +378,9 @@ qint64 CueParser::IndexToMarker(const QString &index) {
}
void CueParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
void CueParser::Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
Q_UNUSED(playlist_name);
Q_UNUSED(songs);
Q_UNUSED(device);
Q_UNUSED(dir);

View File

@@ -56,8 +56,8 @@ class CueParser : public ParserBase {
bool TryMagic(const QByteArray &data) const override;
SongList Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
LoadResult Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const override;
void Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
static QString FindCueFilename(const QString &filename);

View File

@@ -42,7 +42,7 @@ class CollectionBackendInterface;
M3UParser::M3UParser(const SharedPtr<TagReaderClient> tagreader_client, const SharedPtr<CollectionBackendInterface> collection_backend, QObject *parent)
: ParserBase(tagreader_client, collection_backend, parent) {}
SongList M3UParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
ParserBase::LoadResult M3UParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
Q_UNUSED(playlist_path);
@@ -125,7 +125,9 @@ bool M3UParser::ParseMetadata(const QString &line, M3UParser::Metadata *metadata
}
void M3UParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
void M3UParser::Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
Q_UNUSED(playlist_name)
device->write("#EXTM3U\n");

View File

@@ -53,8 +53,8 @@ class M3UParser : public ParserBase {
bool TryMagic(const QByteArray &data) const override;
SongList Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
LoadResult Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const override;
void Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
private:
enum class M3UType {

View File

@@ -46,6 +46,13 @@ class ParserBase : public QObject {
public:
explicit ParserBase(const SharedPtr<TagReaderClient> tagreader_client, const SharedPtr<CollectionBackendInterface> collection_backend, QObject *parent = nullptr);
class LoadResult {
public:
LoadResult(const SongList &_songs = SongList(), const QString &_playlist_name = QString()) : songs(_songs), playlist_name(_playlist_name) {}
SongList songs;
QString playlist_name;
};
virtual QString name() const = 0;
virtual QStringList file_extensions() const = 0;
virtual bool load_supported() const = 0;
@@ -59,8 +66,8 @@ class ParserBase : public QObject {
// This method might not return all the songs found in the playlist.
// Any playlist parser may decide to leave out some entries if it finds them incomplete or invalid.
// This means that the final resulting SongList should be considered valid (at least from the parser's point of view).
virtual SongList Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const = 0;
virtual void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const = 0;
virtual LoadResult Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const = 0;
virtual void Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const = 0;
Q_SIGNALS:
void Error(const QString &error) const;

View File

@@ -195,7 +195,7 @@ SongList PlaylistParser::LoadFromFile(const QString &filename) const {
return SongList();
}
const SongList songs = parser->Load(&file, filename, fileinfo.absolutePath(), true);
const SongList songs = parser->Load(&file, filename, fileinfo.absolutePath(), true).songs;
file.close();
return songs;
@@ -210,11 +210,11 @@ SongList PlaylistParser::LoadFromDevice(QIODevice *device, const QString &path_h
return SongList();
}
return parser->Load(device, path_hint, dir_hint);
return parser->Load(device, path_hint, dir_hint).songs;
}
void PlaylistParser::Save(const SongList &songs, const QString &filename, const PlaylistSettings::PathType path_type) const {
void PlaylistParser::Save(const QString &playlist_name, const SongList &songs, const QString &filename, const PlaylistSettings::PathType path_type) const {
QFileInfo fileinfo(filename);
QDir dir(fileinfo.path());
@@ -248,7 +248,7 @@ void PlaylistParser::Save(const SongList &songs, const QString &filename, const
return;
}
parser->Save(songs, &file, dir, path_type);
parser->Save(playlist_name, songs, &file, dir, path_type);
file.close();

View File

@@ -66,7 +66,7 @@ class PlaylistParser : public QObject {
SongList LoadFromFile(const QString &filename) const;
SongList LoadFromDevice(QIODevice *device, const QString &path_hint = QString(), const QDir &dir_hint = QDir()) const;
void Save(const SongList &songs, const QString &filename, const PlaylistSettings::PathType) const;
void Save(const QString &playlist_name, const SongList &songs, const QString &filename, const PlaylistSettings::PathType) const;
Q_SIGNALS:
void Error(const QString &error) const;

View File

@@ -41,7 +41,7 @@ class CollectionBackendInterface;
PLSParser::PLSParser(const SharedPtr<TagReaderClient> tagreader_client, const SharedPtr<CollectionBackendInterface> collection_backend, QObject *parent)
: ParserBase(tagreader_client, collection_backend, parent) {}
SongList PLSParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
ParserBase::LoadResult PLSParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
Q_UNUSED(playlist_path);
@@ -83,7 +83,9 @@ SongList PLSParser::Load(QIODevice *device, const QString &playlist_path, const
}
void PLSParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
void PLSParser::Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
Q_UNUSED(playlist_name)
QTextStream s(device);
s << "[playlist]" << Qt::endl;

View File

@@ -52,8 +52,8 @@ class PLSParser : public ParserBase {
bool TryMagic(const QByteArray &data) const override;
SongList Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
LoadResult Load(QIODevice *device, const QString &playlist_path = QLatin1String(""), const QDir &dir = QDir(), const bool collection_lookup = true) const override;
void Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir = QDir(), const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
};
#endif // PLSPARSER_H

View File

@@ -46,21 +46,21 @@ bool WplParser::TryMagic(const QByteArray &data) const {
return data.contains("<?wpl") || data.contains("<smil>");
}
SongList WplParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
ParserBase::LoadResult WplParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
Q_UNUSED(playlist_path);
SongList ret;
QXmlStreamReader reader(device);
if (!Utilities::ParseUntilElement(&reader, u"smil"_s) || !Utilities::ParseUntilElement(&reader, u"body"_s)) {
return ret;
return LoadResult();
}
SongList songs;
while (!reader.atEnd() && Utilities::ParseUntilElement(&reader, u"seq"_s)) {
ParseSeq(dir, &reader, &ret, collection_lookup);
ParseSeq(dir, &reader, &songs, collection_lookup);
}
return ret;
return songs;
}
@@ -98,7 +98,9 @@ void WplParser::ParseSeq(const QDir &dir, QXmlStreamReader *reader, SongList *so
}
void WplParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
void WplParser::Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
Q_UNUSED(playlist_name)
QXmlStreamWriter writer(device);
writer.setAutoFormatting(true);

View File

@@ -55,8 +55,8 @@ class WplParser : public XMLParser {
bool TryMagic(const QByteArray &data) const override;
SongList Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup = true) const override;
void Save(const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
LoadResult Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup = true) const override;
void Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type = PlaylistSettings::PathType::Automatic) const override;
private:
void ParseSeq(const QDir &dir, QXmlStreamReader *reader, SongList *songs, const bool collection_lookup) const;

View File

@@ -44,17 +44,27 @@ class CollectionBackendInterface;
XSPFParser::XSPFParser(const SharedPtr<TagReaderClient> tagreader_client, const SharedPtr<CollectionBackendInterface> collection_backend, QObject *parent)
: XMLParser(tagreader_client, collection_backend, parent) {}
SongList XSPFParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
ParserBase::LoadResult XSPFParser::Load(QIODevice *device, const QString &playlist_path, const QDir &dir, const bool collection_lookup) const {
Q_UNUSED(playlist_path);
SongList songs;
QXmlStreamReader reader(device);
if (!Utilities::ParseUntilElement(&reader, u"playlist"_s) || !Utilities::ParseUntilElement(&reader, u"trackList"_s)) {
return songs;
QString playlist_name;
{
QXmlStreamReader reader(device);
if (Utilities::ParseUntilElement(&reader, u"playlist"_s) && Utilities::ParseUntilElement(&reader, u"title"_s)) {
playlist_name = reader.readElementText();
}
}
device->seek(0);
QXmlStreamReader reader(device);
if (!Utilities::ParseUntilElement(&reader, u"playlist"_s)) {
return LoadResult();
}
if (!Utilities::ParseUntilElement(&reader, u"trackList"_s)) {
return LoadResult();
}
SongList songs;
while (!reader.atEnd() && Utilities::ParseUntilElement(&reader, u"track"_s)) {
const Song song = ParseTrack(&reader, dir, collection_lookup);
if (song.is_valid()) {
@@ -62,7 +72,7 @@ SongList XSPFParser::Load(QIODevice *device, const QString &playlist_path, const
}
}
return songs;
return LoadResult(songs, playlist_name);
}
@@ -140,7 +150,7 @@ return_song:
}
void XSPFParser::Save(const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
void XSPFParser::Save(const QString &playlist_name, const SongList &songs, QIODevice *device, const QDir &dir, const PlaylistSettings::PathType path_type) const {
QXmlStreamWriter writer(device);
writer.setAutoFormatting(true);
@@ -150,6 +160,8 @@ void XSPFParser::Save(const SongList &songs, QIODevice *device, const QDir &dir,
writer.writeAttribute("version"_L1, "1"_L1);
writer.writeDefaultNamespace("http://xspf.org/ns/0/"_L1);
writer.writeTextElement("title"_L1, playlist_name);
Settings s;
s.beginGroup(PlaylistSettings::kSettingsGroup);
bool write_metadata = s.value(PlaylistSettings::kWriteMetadata, true).toBool();

Some files were not shown because too many files have changed in this diff Show More