Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4683db900 | ||
|
|
5a90643ba8 | ||
|
|
4a1ab2f004 | ||
|
|
36be755a78 | ||
|
|
ffd8c88b1f | ||
|
|
84ff163b34 | ||
|
|
811e625618 | ||
|
|
037b0d7dea | ||
|
|
d578d3c66d | ||
|
|
2644dbd5ab | ||
|
|
245103dc30 |
302
.github/workflows/build.yml
vendored
302
.github/workflows/build.yml
vendored
@@ -10,29 +10,33 @@ jobs:
|
||||
|
||||
build-opensuse:
|
||||
name: Build openSUSE
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
opensuse_version: [ 'tumbleweed', 'leap:15.6' ]
|
||||
qt_version: [ '5', '6' ]
|
||||
container:
|
||||
image: opensuse/${{matrix.opensuse_version}}
|
||||
steps:
|
||||
- name: Add tagparser repo
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
run: zypper -n ar -c -f -n 'repo-tagparser' https://download.opensuse.org/repositories/home:/mkittler/openSUSE_Tumbleweed/ repo-tagparser
|
||||
- name: Refresh repositories
|
||||
run: zypper -n --gpg-auto-import-keys ref
|
||||
- name: Upgrade packages (Tumbleweed)
|
||||
- name: Upgrade packages
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
run: zypper -n --gpg-auto-import-keys dup
|
||||
- name: Upgrade packages (Leap)
|
||||
- name: Upgrade packages
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
run: zypper -n --gpg-auto-import-keys up
|
||||
- name: Install gcc (Tumbleweed)
|
||||
- name: Install gcc
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
|
||||
- name: Install gcc (Leap)
|
||||
- name: Install gcc 13
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
|
||||
run: zypper -n --gpg-auto-import-keys in gcc13 gcc13-c++
|
||||
- name: Install packages
|
||||
run: >
|
||||
zypper -n --gpg-auto-import-keys in
|
||||
@@ -42,18 +46,22 @@ jobs:
|
||||
tar
|
||||
make
|
||||
cmake
|
||||
gettext-tools
|
||||
openssh-clients
|
||||
glibc-devel
|
||||
libboost_headers-devel
|
||||
boost-devel
|
||||
glib2-devel
|
||||
glib2-tools
|
||||
dbus-1-devel
|
||||
alsa-devel
|
||||
libnotify-devel
|
||||
protobuf-devel
|
||||
sqlite3-devel
|
||||
libpulse-devel
|
||||
gstreamer-devel
|
||||
gstreamer-plugins-base-devel
|
||||
vlc-devel
|
||||
taglib-devel
|
||||
libicu-devel
|
||||
libcdio-devel
|
||||
@@ -66,9 +74,28 @@ jobs:
|
||||
update-desktop-files
|
||||
appstream-glib
|
||||
hicolor-icon-theme
|
||||
- name: Install Qt 5
|
||||
if: matrix.qt_version == '5'
|
||||
run: >
|
||||
zypper -n --gpg-auto-import-keys in
|
||||
libQt5Core-devel
|
||||
libQt5Gui-devel
|
||||
libQt5Widgets-devel
|
||||
libQt5Concurrent-devel
|
||||
libQt5Network-devel
|
||||
libQt5Sql-devel
|
||||
libQt5DBus-devel
|
||||
libQt5Test-devel
|
||||
libqt5-qtbase-common-devel
|
||||
libQt5Sql5-sqlite
|
||||
libqt5-linguist-devel
|
||||
libqt5-qtx11extras-devel
|
||||
- name: Install Qt 6
|
||||
if: matrix.qt_version == '6'
|
||||
run: >
|
||||
zypper -n --gpg-auto-import-keys in
|
||||
qt6-core-devel
|
||||
qt6-gui-devel
|
||||
qt6-gui-private-devel
|
||||
qt6-widgets-devel
|
||||
qt6-concurrent-devel
|
||||
qt6-network-devel
|
||||
@@ -78,10 +105,14 @@ jobs:
|
||||
qt6-base-common-devel
|
||||
qt6-sql-sqlite
|
||||
qt6-linguist-devel
|
||||
gtest
|
||||
gmock
|
||||
- name: Install kdsingleapplication-qt6-devel
|
||||
- name: Install tagparser
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
run: zypper -n --gpg-auto-import-keys in tagparser-devel
|
||||
- name: Install kdsingleapplication-devel
|
||||
if: matrix.opensuse_version == 'tumbleweed' && matrix.qt_version == '5'
|
||||
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-devel
|
||||
- name: Install kdsingleapplication-qt6-devel
|
||||
if: matrix.opensuse_version == 'tumbleweed' && matrix.qt_version == '6'
|
||||
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -93,7 +124,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 -DBUILD_WERROR=ON -DUSE_TAGLIB=ON -DQT_VERSION_MAJOR=${{matrix.qt_version}}
|
||||
- name: Create source tarball
|
||||
working-directory: build
|
||||
run: ../dist/scripts/maketarball.sh
|
||||
@@ -104,30 +135,27 @@ 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 strawberry.spec
|
||||
run: rpmbuild -ba ../dist/unix/strawberry.spec
|
||||
- name: Build RPM (Leap)
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
env:
|
||||
RPM_BUILD_NCPUS: 4
|
||||
CC: gcc-14
|
||||
CXX: g++-14
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
env:
|
||||
CC: gcc-13
|
||||
CXX: g++-13
|
||||
run: rpmbuild -ba ../dist/unix/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
|
||||
- name: Upload source
|
||||
if: matrix.opensuse_version == 'tumbleweed'
|
||||
if: matrix.opensuse_version == 'tumbleweed' && matrix.qt_version == '6'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: source
|
||||
path: |
|
||||
/usr/src/packages/SOURCES/*.xz
|
||||
- name: Upload rpm
|
||||
if: matrix.opensuse_version != 'tumbleweed'
|
||||
if: matrix.opensuse_version != 'tumbleweed' && matrix.qt_version == '6'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: opensuse-${{steps.set-subdir.outputs.subdir}}
|
||||
@@ -138,12 +166,12 @@ jobs:
|
||||
|
||||
build-fedora:
|
||||
name: Build Fedora
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
fedora_version: [ '39', '40', '41', '42' ]
|
||||
fedora_version: [ '39', '40', '41' ]
|
||||
container:
|
||||
image: fedora:${{matrix.fedora_version}}
|
||||
steps:
|
||||
@@ -155,7 +183,7 @@ jobs:
|
||||
run: >
|
||||
dnf -y install
|
||||
@development-tools
|
||||
lsb_release
|
||||
redhat-lsb-core
|
||||
which
|
||||
git
|
||||
glibc
|
||||
@@ -167,9 +195,13 @@ jobs:
|
||||
glib
|
||||
man
|
||||
tar
|
||||
gettext
|
||||
openssh
|
||||
rsync
|
||||
boost-devel
|
||||
dbus-devel
|
||||
protobuf-devel
|
||||
protobuf-compiler
|
||||
sqlite-devel
|
||||
alsa-lib-devel
|
||||
pulseaudio-libs-devel
|
||||
@@ -191,8 +223,6 @@ jobs:
|
||||
libappstream-glib
|
||||
hicolor-icon-theme
|
||||
kdsingleapplication-qt6-devel
|
||||
gtest-devel
|
||||
gmock-devel
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -215,9 +245,9 @@ jobs:
|
||||
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
|
||||
- name: Build RPM
|
||||
env:
|
||||
RPM_BUILD_NCPUS: 4
|
||||
RPM_BUILD_NCPUS: "2"
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
run: rpmbuild -ba ../dist/unix/strawberry.spec
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -229,7 +259,7 @@ jobs:
|
||||
|
||||
build-openmandriva:
|
||||
name: Build OpenMandriva
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master' && false
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && false
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -251,11 +281,15 @@ jobs:
|
||||
make
|
||||
cmake
|
||||
glib
|
||||
gettext
|
||||
lsb-release
|
||||
rpmdevtools
|
||||
rpm-build
|
||||
glibc-devel
|
||||
boost-devel
|
||||
dbus-devel
|
||||
protobuf-devel
|
||||
protobuf-compiler
|
||||
sqlite-devel
|
||||
libasound-devel
|
||||
pulseaudio-devel
|
||||
@@ -308,9 +342,9 @@ jobs:
|
||||
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
|
||||
- name: Build RPM
|
||||
env:
|
||||
RPM_BUILD_NCPUS: 4
|
||||
RPM_BUILD_NCPUS: "2"
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
run: rpmbuild -ba ../dist/unix/strawberry.spec
|
||||
- name: Upload artifacts
|
||||
if: matrix.openmandriva_version != 'cooker'
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -323,7 +357,7 @@ jobs:
|
||||
|
||||
build-mageia:
|
||||
name: Build Mageia
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -354,7 +388,9 @@ jobs:
|
||||
man
|
||||
tar
|
||||
rpmdevtools
|
||||
gettext
|
||||
lib64boost-devel
|
||||
lib64protobuf-devel
|
||||
lib64sqlite3-devel
|
||||
lib64alsa2-devel
|
||||
lib64pulseaudio-devel
|
||||
@@ -379,10 +415,10 @@ jobs:
|
||||
lib64qt6dbus-devel
|
||||
lib64qt6help-devel
|
||||
lib64qt6test-devel
|
||||
protobuf-compiler
|
||||
desktop-file-utils
|
||||
appstream-util
|
||||
hicolor-icon-theme
|
||||
gtest
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -405,9 +441,9 @@ jobs:
|
||||
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
|
||||
- name: Build RPM
|
||||
env:
|
||||
RPM_BUILD_NCPUS: 4
|
||||
RPM_BUILD_NCPUS: "2"
|
||||
working-directory: build
|
||||
run: rpmbuild -ba strawberry.spec
|
||||
run: rpmbuild -ba ../dist/unix/strawberry.spec
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -419,12 +455,12 @@ jobs:
|
||||
|
||||
build-debian:
|
||||
name: Build Debian
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
debian_version: [ 'bookworm', 'trixie' ]
|
||||
debian_version: [ 'bullseye', 'bookworm', 'trixie' ]
|
||||
container:
|
||||
image: debian:${{matrix.debian_version}}
|
||||
steps:
|
||||
@@ -446,10 +482,14 @@ jobs:
|
||||
g++
|
||||
pkg-config
|
||||
fakeroot
|
||||
gettext
|
||||
lsb-release
|
||||
dpkg-dev
|
||||
libglib2.0-dev
|
||||
libdbus-1-dev
|
||||
libboost-dev
|
||||
libprotobuf-dev
|
||||
protobuf-compiler
|
||||
libsqlite3-dev
|
||||
libasound2-dev
|
||||
libpulse-dev
|
||||
@@ -465,13 +505,16 @@ jobs:
|
||||
libcdio-dev
|
||||
libmtp-dev
|
||||
libgpod-dev
|
||||
libxkbcommon-dev
|
||||
qt6-base-dev
|
||||
qt6-base-private-dev
|
||||
qt6-base-dev-tools
|
||||
qt6-tools-dev
|
||||
qt6-tools-dev-tools
|
||||
qt6-l10n-tools
|
||||
- name: Install Qt 5
|
||||
if: matrix.debian_version == 'bullseye'
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev
|
||||
- name: Install Qt 6
|
||||
if: matrix.debian_version != 'bullseye'
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -482,29 +525,26 @@ jobs:
|
||||
- name: Create Build Environment
|
||||
run: cmake -E make_directory build
|
||||
- name: Configure CMake
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
|
||||
- name: Delete build directory
|
||||
run: rm -rf build
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_WERROR=ON
|
||||
- name: make deb
|
||||
run: dpkg-buildpackage -b -d -uc -us -nc -j4
|
||||
run: dpkg-buildpackage -b -d -uc -us -nc -j2
|
||||
- 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.ref != 'refs/heads/l10n_master'
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'oracular' ]
|
||||
ubuntu_version: [ 'focal', 'jammy', 'noble', 'oracular' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -528,10 +568,14 @@ jobs:
|
||||
fakeroot
|
||||
wget
|
||||
curl
|
||||
gettext
|
||||
lsb-release
|
||||
dpkg-dev
|
||||
libglib2.0-dev
|
||||
libboost-dev
|
||||
libdbus-1-dev
|
||||
libprotobuf-dev
|
||||
protobuf-compiler
|
||||
libsqlite3-dev
|
||||
libasound2-dev
|
||||
libpulse-dev
|
||||
@@ -548,13 +592,16 @@ jobs:
|
||||
libcdio-dev
|
||||
libmtp-dev
|
||||
libgpod-dev
|
||||
libxkbcommon-dev
|
||||
qt6-base-dev
|
||||
qt6-base-private-dev
|
||||
qt6-base-dev-tools
|
||||
qt6-tools-dev
|
||||
qt6-tools-dev-tools
|
||||
qt6-l10n-tools
|
||||
- name: Install Qt 5
|
||||
if: matrix.ubuntu_version == 'focal'
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev
|
||||
- name: Install Qt 6
|
||||
if: matrix.ubuntu_version != 'focal'
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -565,11 +612,9 @@ jobs:
|
||||
- name: Create Build Environment
|
||||
run: cmake -E make_directory build
|
||||
- name: Configure CMake
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
|
||||
- name: Delete build directory
|
||||
run: rm -rf build
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_WERROR=ON
|
||||
- name: make deb
|
||||
run: dpkg-buildpackage -b -d -uc -us -nc -j4
|
||||
run: dpkg-buildpackage -b -d -uc -us -nc -j2
|
||||
- name: Copy deb
|
||||
run: cp ../*.deb ../*.ddeb .
|
||||
- name: Upload artifacts
|
||||
@@ -588,7 +633,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'oracular' ]
|
||||
ubuntu_version: [ 'focal', 'jammy', 'noble', 'oracular' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -610,12 +655,15 @@ jobs:
|
||||
gcc
|
||||
g++
|
||||
fakeroot
|
||||
gettext
|
||||
lsb-release
|
||||
gpg
|
||||
dput
|
||||
dpkg-dev
|
||||
libglib2.0-dev
|
||||
libboost-dev
|
||||
libdbus-1-dev
|
||||
libprotobuf-dev
|
||||
libsqlite3-dev
|
||||
libasound2-dev
|
||||
libpulse-dev
|
||||
@@ -630,20 +678,24 @@ jobs:
|
||||
libcdio-dev
|
||||
libmtp-dev
|
||||
libgpod-dev
|
||||
libxkbcommon-dev
|
||||
qt6-base-dev
|
||||
qt6-base-private-dev
|
||||
qt6-base-dev-tools
|
||||
qt6-tools-dev
|
||||
qt6-tools-dev-tools
|
||||
qt6-l10n-tools
|
||||
gstreamer1.0-alsa
|
||||
gstreamer1.0-pulseaudio
|
||||
protobuf-compiler
|
||||
- name: Install keyboxd
|
||||
if: matrix.ubuntu_version == 'noble'
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y keyboxd
|
||||
- name: Install Qt 5
|
||||
if: matrix.ubuntu_version == 'focal'
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y qtbase5-dev qtbase5-dev-tools qttools5-dev qttools5-dev-tools libqt5x11extras5-dev
|
||||
- name: Install Qt 6
|
||||
if: matrix.ubuntu_version != 'focal'
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -654,7 +706,7 @@ jobs:
|
||||
- name: Create Build Environment
|
||||
run: cmake -E make_directory build
|
||||
- name: Configure CMake
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_WERROR=ON
|
||||
- name: Delete build directory
|
||||
run: rm -rf build
|
||||
- name: Import Ubuntu PPA GPG private key
|
||||
@@ -671,60 +723,9 @@ 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.ref != 'refs/heads/l10n_master'
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private'
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -806,14 +807,14 @@ jobs:
|
||||
-B build
|
||||
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
|
||||
-DCMAKE_PREFIX_PATH="${{env.prefix_path}}/lib/cmake"
|
||||
-DBUILD_WERROR=ON
|
||||
-DBUILD_WITH_QT6=ON
|
||||
-DBUILD_WERROR=OFF
|
||||
-DUSE_BUNDLE=ON
|
||||
-DENABLE_DBUS=OFF
|
||||
-DICU_ROOT="${{env.prefix_path}}"
|
||||
-DFFTW3_DIR="${{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
|
||||
@@ -834,7 +835,7 @@ jobs:
|
||||
- name: Manually Codesign
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-13'
|
||||
working-directory: build
|
||||
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
||||
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libutf8_validity.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
|
||||
|
||||
- name: Manually Codesign
|
||||
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-14'
|
||||
@@ -947,14 +948,14 @@ jobs:
|
||||
-B build
|
||||
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
|
||||
-DCMAKE_PREFIX_PATH="${{env.prefix_path}}/lib/cmake"
|
||||
-DBUILD_WERROR=ON
|
||||
-DBUILD_WITH_QT6=ON
|
||||
-DBUILD_WERROR=OFF
|
||||
-DUSE_BUNDLE=ON
|
||||
-DENABLE_DBUS=OFF
|
||||
-DICU_ROOT="${{env.prefix_path}}"
|
||||
-DFFTW3_DIR="${{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
|
||||
@@ -1006,12 +1007,12 @@ jobs:
|
||||
|
||||
build-windows-mingw:
|
||||
name: Build Windows MinGW
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [ 'i686', 'x86_64' ]
|
||||
arch: [ 'x86_64' ]
|
||||
buildtype: [ 'debug', 'release' ]
|
||||
container:
|
||||
image: jonaski/strawberry-mxe-${{matrix.arch}}-${{matrix.buildtype}}
|
||||
@@ -1046,14 +1047,16 @@ jobs:
|
||||
-DCMAKE_TOOLCHAIN_FILE="../cmake/Toolchain-${{matrix.arch}}-w64-mingw32-shared.cmake"
|
||||
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
|
||||
-DCMAKE_PREFIX_PATH="/strawberry-mxe/usr/${{matrix.arch}}-w64-mingw32.shared/qt6"
|
||||
-DBUILD_WERROR=ON
|
||||
-DBUILD_WITH_QT6=ON
|
||||
-DBUILD_WERROR=OFF
|
||||
-DARCH="${{matrix.arch}}"
|
||||
-DENABLE_WIN32_CONSOLE=$(test "${{matrix.buildtype}}" = "debug" && echo "ON" || echo "OFF")
|
||||
-DENABLE_GIO=OFF
|
||||
-DENABLE_DBUS=OFF
|
||||
-DENABLE_LIBGPOD=OFF
|
||||
-DENABLE_LIBMTP=OFF
|
||||
-DENABLE_AUDIOCD=OFF
|
||||
-DENABLE_MTP=OFF
|
||||
-DENABLE_GPOD=OFF
|
||||
-DENABLE_SPOTIFY=OFF
|
||||
-DProtobuf_PROTOC_EXECUTABLE="/strawberry-mxe/usr/x86_64-pc-linux-gnu/bin/protoc"
|
||||
|
||||
- name: Run Make
|
||||
run: cmake --build build --config "${{env.cmake_buildtype}}" --parallel $(nproc)
|
||||
@@ -1126,7 +1129,7 @@ jobs:
|
||||
|
||||
- name: Copy nsis files
|
||||
working-directory: build
|
||||
run: cp ${GITHUB_WORKSPACE}/dist/windows/*.nsh ${GITHUB_WORKSPACE}/dist/windows/*.ico .
|
||||
run: cp ${GITHUB_WORKSPACE}/dist/windows/*.nsi ${GITHUB_WORKSPACE}/dist/windows/*.nsh ${GITHUB_WORKSPACE}/dist/windows/*.ico .
|
||||
|
||||
- name: Copy COPYING license file
|
||||
working-directory: build
|
||||
@@ -1195,12 +1198,12 @@ jobs:
|
||||
|
||||
build-windows-msvc:
|
||||
name: Build Windows MSVC
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
|
||||
if: github.repository != 'strawberrymusicplayer/strawberry-private'
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [ 'x86', 'x86_64' ]
|
||||
arch: [ 'x86_64' ]
|
||||
buildtype: [ 'release' ]
|
||||
steps:
|
||||
|
||||
@@ -1333,15 +1336,14 @@ jobs:
|
||||
-G "Ninja"
|
||||
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
|
||||
-DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake"
|
||||
-DBUILD_WITH_QT6=ON
|
||||
-DARCH="${{matrix.arch}}"
|
||||
-DENABLE_WIN32_CONSOLE=${{env.win32_console}}
|
||||
-DUSE_TAGLIB=ON
|
||||
-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
|
||||
-DFFTW3_DIR="${{env.prefix_path_forwardslash}}"
|
||||
-DBoost_INCLUDE_DIR="${{env.prefix_path_forwardslash}}/include"
|
||||
|
||||
- name: Run Make
|
||||
shell: cmd
|
||||
@@ -1435,6 +1437,7 @@ jobs:
|
||||
shell: cmd
|
||||
working-directory: build
|
||||
run: |
|
||||
copy ..\dist\windows\*.nsi .
|
||||
copy ..\dist\windows\*.nsh .
|
||||
copy ..\dist\windows\*.ico .
|
||||
|
||||
@@ -1475,11 +1478,6 @@ 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
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,4 +11,8 @@
|
||||
/out
|
||||
/CMakeSettings.json
|
||||
/dist/scripts/maketarball.sh
|
||||
/dist/unix/strawberry.spec
|
||||
/dist/windows/strawberry.nsi
|
||||
/debian/control
|
||||
/debian/changelog
|
||||
/dist/macos/Info.plist
|
||||
|
||||
17
3rdparty/README.md
vendored
17
3rdparty/README.md
vendored
@@ -9,3 +9,20 @@ It is also used to pass command-line options through to the first instance.
|
||||
This 3rdparty copy is used only if KDSingleApplication 1.1 or higher is not found on the system.
|
||||
|
||||
URL: https://github.com/KDAB/KDSingleApplication/
|
||||
|
||||
|
||||
SPMediaKeyTap
|
||||
-------------
|
||||
A library used on macOS to exclusively grab global media shortcuts.
|
||||
|
||||
The library is no longer maintained by the original author.
|
||||
|
||||
The directory can safely be deleted on other platforms.
|
||||
|
||||
getopt
|
||||
------
|
||||
getopt included on Windows for command line options parsing with Unicode support .
|
||||
|
||||
The directory can safely be deleted on other platforms.
|
||||
|
||||
URL: https://github.com/ludvikjerabek/getopt-win
|
||||
|
||||
11
3rdparty/SPMediaKeyTap/CMakeLists.txt
vendored
Normal file
11
3rdparty/SPMediaKeyTap/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
set(SPMEDIAKEY-SOURCES
|
||||
SPMediaKeyTap.m
|
||||
)
|
||||
|
||||
set(SPMEDIAKEY-HEADERS
|
||||
SPMediaKeyTap.h
|
||||
)
|
||||
|
||||
ADD_LIBRARY(SPMediaKeyTap STATIC
|
||||
${SPMEDIAKEY-SOURCES}
|
||||
)
|
||||
8
3rdparty/SPMediaKeyTap/LICENSE
vendored
Normal file
8
3rdparty/SPMediaKeyTap/LICENSE
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
Copyright (c) 2011, Joachim Bengtsson
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Neither the name of the organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
12
3rdparty/SPMediaKeyTap/README.md
vendored
Normal file
12
3rdparty/SPMediaKeyTap/README.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
SPMediaKeyTap
|
||||
=============
|
||||
|
||||
`SPMediaKeyTap` abstracts a `CGEventHook` and other nastiness in order to give you a relatively simple API to receive media key events (prev/next/playpause, on F7 to F9 on modern MacBook Pros) exclusively, without them reaching other applications like iTunes. `SPMediaKeyTap` is clever enough to resign its exclusive lock on media keys by looking for which application was active most recently: if that application is in `SPMediaKeyTap`'s whitelist, it will resign the keys. This is similar to the behavior of Apple's applications collaborating on media key handling exclusivity, but unfortunately, Apple is not exposing any APIs allowing third-parties to join in on this collaboration.
|
||||
|
||||
For now, the whitelist is just a hardcoded array in `+[SPMediaKeyTap defaultMediaKeyUserBundleIdentifiers]`. If your app starts using `SPMediaKeyTap`, please [mail me](mailto:nevyn@spotify.com) your bundle ID, and I'll include it in the canonical repository. This is a bad solution; a better solution would be to use distributed notifications to collaborate in creating this whitelist at runtime. Hopefully someone'll have the time and energy to write this soon.
|
||||
|
||||
In `Example/SPMediaKeyTapExampleAppDelegate.m` is an example of both how you use `SPMediaKeyTap`, and how you handle the semi-private `NSEvent` subtypes involved in media keys, including on how to fall back to non-event tap handling of these events.
|
||||
|
||||
`SPMediaKeyTap` and other `CGEventHook`s on the event type `NSSystemDefined` is known to interfere with each other and applications doing weird stuff with mouse input, because mouse clicks are also part of the `NSSystemDefined` category. The single issue we have had reported here at Spotify is Adobe Fireworks, in which item selection stops working with `SPMediaKeyTap` is active.
|
||||
|
||||
`SPMediaKeyTap` requires 10.5 to work, and disables itself on 10.4.
|
||||
53
3rdparty/SPMediaKeyTap/SPMediaKeyTap.h
vendored
Normal file
53
3rdparty/SPMediaKeyTap/SPMediaKeyTap.h
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright (c) 2011, Joachim Bengtsson
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Neither the name of the organization nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <IOKit/hidsystem/ev_keymap.h>
|
||||
|
||||
// http://overooped.com/post/2593597587/mediakeys
|
||||
|
||||
#define SPSystemDefinedEventMediaKeys 8
|
||||
|
||||
@interface SPMediaKeyTap : NSObject
|
||||
|
||||
- (id)initWithDelegate:(id)delegate;
|
||||
|
||||
+ (BOOL)usesGlobalMediaKeyTap;
|
||||
- (BOOL)startWatchingMediaKeys;
|
||||
- (void)stopWatchingMediaKeys;
|
||||
- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event;
|
||||
@end
|
||||
|
||||
@interface NSObject (SPMediaKeyTapDelegate)
|
||||
- (void)mediaKeyTap:(SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event;
|
||||
@end
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern NSString *kIgnoreMediaKeysDefaultsKey;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
361
3rdparty/SPMediaKeyTap/SPMediaKeyTap.m
vendored
Normal file
361
3rdparty/SPMediaKeyTap/SPMediaKeyTap.m
vendored
Normal file
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
Copyright (c) 2011, Joachim Bengtsson
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Neither the name of the organization nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
// Copyright (c) 2010 Spotify AB
|
||||
#import "SPMediaKeyTap.h"
|
||||
|
||||
// Define to enable app list debug output
|
||||
// #define DEBUG_SPMEDIAKEY_APPLIST 1
|
||||
|
||||
NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
|
||||
|
||||
@interface SPMediaKeyTap () {
|
||||
CFMachPortRef _eventPort;
|
||||
CFRunLoopSourceRef _eventPortSource;
|
||||
CFRunLoopRef _tapThreadRL;
|
||||
NSThread *_tapThread;
|
||||
BOOL _shouldInterceptMediaKeyEvents;
|
||||
id _delegate;
|
||||
// The app that is frontmost in this list owns media keys
|
||||
NSMutableArray<NSRunningApplication *> *_mediaKeyAppList;
|
||||
}
|
||||
|
||||
- (BOOL)shouldInterceptMediaKeyEvents;
|
||||
- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
|
||||
- (void)startWatchingAppSwitching;
|
||||
- (void)stopWatchingAppSwitching;
|
||||
- (void)eventTapThread;
|
||||
@end
|
||||
|
||||
static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
|
||||
|
||||
|
||||
// Inspired by http://gist.github.com/546311
|
||||
|
||||
@implementation SPMediaKeyTap
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Setup and teardown
|
||||
|
||||
- (id)initWithDelegate:(id)delegate
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_delegate = delegate;
|
||||
[self startWatchingAppSwitching];
|
||||
_mediaKeyAppList = [NSMutableArray new];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self stopWatchingMediaKeys];
|
||||
[self stopWatchingAppSwitching];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (void)startWatchingAppSwitching
|
||||
{
|
||||
// Listen to "app switched" event, so that we don't intercept media keys if we
|
||||
// weren't the last "media key listening" app to be active
|
||||
|
||||
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
|
||||
selector:@selector(frontmostAppChanged:)
|
||||
name:NSWorkspaceDidActivateApplicationNotification
|
||||
object:nil];
|
||||
|
||||
|
||||
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
|
||||
selector:@selector(appTerminated:)
|
||||
name:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)stopWatchingAppSwitching
|
||||
{
|
||||
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (BOOL)startWatchingMediaKeys
|
||||
{
|
||||
// Prevent having multiple mediaKeys threads
|
||||
[self stopWatchingMediaKeys];
|
||||
|
||||
[self setShouldInterceptMediaKeyEvents:YES];
|
||||
|
||||
// Add an event tap to intercept the system defined media key events
|
||||
_eventPort = CGEventTapCreate(kCGSessionEventTap,
|
||||
kCGHeadInsertEventTap,
|
||||
kCGEventTapOptionDefault,
|
||||
CGEventMaskBit(NX_SYSDEFINED),
|
||||
tapEventCallback,
|
||||
(__bridge void * __nullable)(self));
|
||||
|
||||
// Can be NULL if the app has no accessibility access permission
|
||||
if (_eventPort == NULL)
|
||||
return NO;
|
||||
|
||||
_eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
|
||||
assert(_eventPortSource != NULL);
|
||||
|
||||
if (_eventPortSource == NULL)
|
||||
return NO;
|
||||
|
||||
// Let's do this in a separate thread so that a slow app doesn't lag the event tap
|
||||
_tapThread = [[NSThread alloc] initWithTarget:self
|
||||
selector:@selector(eventTapThread)
|
||||
object:nil];
|
||||
[_tapThread start];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)stopWatchingMediaKeys
|
||||
{
|
||||
// Shut down tap thread
|
||||
if(_tapThreadRL){
|
||||
CFRunLoopStop(_tapThreadRL);
|
||||
_tapThreadRL = nil;
|
||||
}
|
||||
|
||||
// Remove tap port
|
||||
if(_eventPort){
|
||||
CFMachPortInvalidate(_eventPort);
|
||||
CFRelease(_eventPort);
|
||||
_eventPort = nil;
|
||||
}
|
||||
|
||||
// Remove tap source
|
||||
if(_eventPortSource){
|
||||
CFRelease(_eventPortSource);
|
||||
_eventPortSource = nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Accessors
|
||||
|
||||
+ (BOOL)usesGlobalMediaKeyTap
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
// breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot
|
||||
return NO;
|
||||
#else
|
||||
// XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
|
||||
return
|
||||
![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey]
|
||||
&& floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (NSArray*)mediaKeyUserBundleIdentifiers
|
||||
{
|
||||
return [NSArray arrayWithObjects:
|
||||
[[NSBundle mainBundle] bundleIdentifier], // your app
|
||||
@"com.spotify.client",
|
||||
@"com.apple.iTunes",
|
||||
@"com.apple.QuickTimePlayerX",
|
||||
@"com.apple.quicktimeplayer",
|
||||
@"com.apple.iWork.Keynote",
|
||||
@"com.apple.iPhoto",
|
||||
@"org.videolan.vlc",
|
||||
@"com.apple.Aperture",
|
||||
@"com.plexsquared.Plex",
|
||||
@"com.soundcloud.desktop",
|
||||
@"org.niltsh.MPlayerX",
|
||||
@"com.ilabs.PandorasHelper",
|
||||
@"com.mahasoftware.pandabar",
|
||||
@"com.bitcartel.pandorajam",
|
||||
@"org.clementine-player.clementine",
|
||||
@"fm.last.Last.fm",
|
||||
@"fm.last.Scrobbler",
|
||||
@"com.beatport.BeatportPro",
|
||||
@"com.Timenut.SongKey",
|
||||
@"com.macromedia.fireworks", // the tap messes up their mouse input
|
||||
@"at.justp.Theremin",
|
||||
@"ru.ya.themblsha.YandexMusic",
|
||||
@"com.jriver.MediaCenter18",
|
||||
@"com.jriver.MediaCenter19",
|
||||
@"com.jriver.MediaCenter20",
|
||||
@"co.rackit.mate",
|
||||
@"com.ttitt.b-music",
|
||||
@"com.beardedspice.BeardedSpice",
|
||||
@"com.plug.Plug",
|
||||
@"com.plug.Plug2",
|
||||
@"com.netease.163music",
|
||||
@"org.quodlibet.quodlibet",
|
||||
nil
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)shouldInterceptMediaKeyEvents
|
||||
{
|
||||
BOOL shouldIntercept = NO;
|
||||
@synchronized(self) {
|
||||
shouldIntercept = _shouldInterceptMediaKeyEvents;
|
||||
}
|
||||
return shouldIntercept;
|
||||
}
|
||||
|
||||
- (void)pauseTapOnTapThread:(NSNumber *)yeahno
|
||||
{
|
||||
CGEventTapEnable(self->_eventPort, [yeahno boolValue]);
|
||||
}
|
||||
|
||||
- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting
|
||||
{
|
||||
BOOL oldSetting;
|
||||
@synchronized(self) {
|
||||
oldSetting = _shouldInterceptMediaKeyEvents;
|
||||
_shouldInterceptMediaKeyEvents = newSetting;
|
||||
}
|
||||
if(_tapThreadRL && oldSetting != newSetting) {
|
||||
[self performSelector:@selector(pauseTapOnTapThread:)
|
||||
onThread:_tapThread
|
||||
withObject:@(newSetting)
|
||||
waitUntilDone:NO];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Event tap callbacks
|
||||
|
||||
// Note: method called on background thread
|
||||
|
||||
static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
|
||||
{
|
||||
#pragma unused(proxy)
|
||||
SPMediaKeyTap *self = (__bridge SPMediaKeyTap *)refcon;
|
||||
|
||||
if(type == kCGEventTapDisabledByTimeout) {
|
||||
NSLog(@"Media key event tap was disabled by timeout");
|
||||
CGEventTapEnable(self->_eventPort, TRUE);
|
||||
return event;
|
||||
} else if(type == kCGEventTapDisabledByUserInput) {
|
||||
// Was disabled manually by -[pauseTapOnTapThread]
|
||||
return event;
|
||||
}
|
||||
NSEvent *nsEvent = nil;
|
||||
@try {
|
||||
nsEvent = [NSEvent eventWithCGEvent:event];
|
||||
}
|
||||
@catch (NSException * e) {
|
||||
NSLog(@"Strange CGEventType: %d: %@", type, e);
|
||||
assert(0);
|
||||
return event;
|
||||
}
|
||||
|
||||
if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
|
||||
return event;
|
||||
|
||||
int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
|
||||
if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
|
||||
return event;
|
||||
|
||||
if (![self shouldInterceptMediaKeyEvents])
|
||||
return event;
|
||||
|
||||
[self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
|
||||
{
|
||||
@autoreleasepool {
|
||||
CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event
|
||||
{
|
||||
[_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
|
||||
}
|
||||
|
||||
- (void)eventTapThread
|
||||
{
|
||||
_tapThreadRL = CFRunLoopGetCurrent();
|
||||
CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
|
||||
CFRunLoopRun();
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark Task switching callbacks
|
||||
|
||||
- (void)mediaKeyAppListChanged
|
||||
{
|
||||
#ifdef DEBUG_SPMEDIAKEY_APPLIST
|
||||
[self debugPrintAppList];
|
||||
#endif
|
||||
|
||||
if([_mediaKeyAppList count] == 0)
|
||||
return;
|
||||
|
||||
NSRunningApplication *thisApp = [NSRunningApplication currentApplication];
|
||||
NSRunningApplication *otherApp = [_mediaKeyAppList firstObject];
|
||||
|
||||
BOOL isCurrent = [thisApp isEqual:otherApp];
|
||||
|
||||
[self setShouldInterceptMediaKeyEvents:isCurrent];
|
||||
}
|
||||
|
||||
- (void)frontmostAppChanged:(NSNotification *)notification
|
||||
{
|
||||
NSRunningApplication *app = [notification.userInfo objectForKey:NSWorkspaceApplicationKey];
|
||||
if (app.bundleIdentifier == nil)
|
||||
return;
|
||||
|
||||
if (![[SPMediaKeyTap mediaKeyUserBundleIdentifiers] containsObject:app.bundleIdentifier])
|
||||
return;
|
||||
|
||||
[_mediaKeyAppList removeObject:app];
|
||||
[_mediaKeyAppList insertObject:app atIndex:0];
|
||||
[self mediaKeyAppListChanged];
|
||||
}
|
||||
|
||||
- (void)appTerminated:(NSNotification *)notification
|
||||
{
|
||||
NSRunningApplication *app = [notification.userInfo objectForKey:NSWorkspaceApplicationKey];
|
||||
[_mediaKeyAppList removeObject:app];
|
||||
|
||||
[self mediaKeyAppListChanged];
|
||||
}
|
||||
|
||||
#ifdef DEBUG_SPMEDIAKEY_APPLIST
|
||||
- (void)debugPrintAppList
|
||||
{
|
||||
NSMutableString *list = [NSMutableString stringWithCapacity:255];
|
||||
for (NSRunningApplication *app in _mediaKeyAppList) {
|
||||
[list appendFormat:@" - %@\n", app.bundleIdentifier];
|
||||
}
|
||||
NSLog(@"List: \n%@", list);
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
3
3rdparty/getopt/CMakeLists.txt
vendored
Normal file
3
3rdparty/getopt/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
add_library(getopt STATIC getopt.cpp)
|
||||
target_compile_definitions(getopt PRIVATE -DSTATIC_GETOPT -D_UNICODE)
|
||||
target_include_directories(getopt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
753
3rdparty/getopt/getopt.cpp
vendored
Normal file
753
3rdparty/getopt/getopt.cpp
vendored
Normal file
@@ -0,0 +1,753 @@
|
||||
/* Getopt for Microsoft C
|
||||
This code is a modification of the Free Software Foundation, Inc.
|
||||
Getopt library for parsing command line argument the purpose was
|
||||
to provide a Microsoft Visual C friendly derivative. This code
|
||||
provides functionality for both Unicode and Multibyte builds.
|
||||
|
||||
Date: 02/03/2011 - Ludvik Jerabek - Initial Release
|
||||
Version: 1.1
|
||||
Comment: Supports getopt, getopt_long, and getopt_long_only
|
||||
and POSIXLY_CORRECT environment flag
|
||||
License: LGPL
|
||||
|
||||
Revisions:
|
||||
|
||||
02/03/2011 - Ludvik Jerabek - Initial Release
|
||||
02/20/2011 - Ludvik Jerabek - Fixed compiler warnings at Level 4
|
||||
07/05/2011 - Ludvik Jerabek - Added no_argument, required_argument, optional_argument defs
|
||||
08/03/2011 - Ludvik Jerabek - Fixed non-argument runtime bug which caused runtime exception
|
||||
08/09/2011 - Ludvik Jerabek - Added code to export functions for DLL and LIB
|
||||
02/15/2012 - Ludvik Jerabek - Fixed _GETOPT_THROW definition missing in implementation file
|
||||
08/01/2012 - Ludvik Jerabek - Created separate functions for char and wchar_t characters so single dll can do both unicode and ansi
|
||||
10/15/2012 - Ludvik Jerabek - Modified to match latest GNU features
|
||||
06/19/2015 - Ludvik Jerabek - Fixed maximum option limitation caused by option_a (255) and option_w (65535) structure val variable
|
||||
09/24/2022 - Ludvik Jerabek - Updated to match most recent getopt release
|
||||
09/25/2022 - Ludvik Jerabek - Fixed memory allocation (malloc call) issue for wchar_t*
|
||||
|
||||
**DISCLAIMER**
|
||||
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
||||
EITHER EXPRESS OR IMPLIED, INCLUDING, BUT Not LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, OR NON-INFRINGEMENT. SOME JURISDICTIONS DO NOT ALLOW THE
|
||||
EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT
|
||||
APPLY TO YOU. IN NO EVENT WILL I BE LIABLE TO ANY PARTY FOR ANY
|
||||
DIRECT, INDIRECT, SPECIAL OR OTHER CONSEQUENTIAL DAMAGES FOR ANY
|
||||
USE OF THIS MATERIAL INCLUDING, WITHOUT LIMITATION, ANY LOST
|
||||
PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR OTHER DATA ON
|
||||
YOUR INFORMATION HANDLING SYSTEM OR OTHERWISE, EVEN If WE ARE
|
||||
EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
*/
|
||||
#define _CRT_SECURE_NO_WARNINGS
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <malloc.h>
|
||||
#include "getopt.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
# define _GETOPT_THROW throw()
|
||||
#else
|
||||
# define _GETOPT_THROW
|
||||
#endif
|
||||
|
||||
int optind = 1;
|
||||
int opterr = 1;
|
||||
int optopt = '?';
|
||||
enum ENUM_ORDERING {
|
||||
REQUIRE_ORDER,
|
||||
PERMUTE,
|
||||
RETURN_IN_ORDER
|
||||
};
|
||||
|
||||
//
|
||||
//
|
||||
// Ansi structures and functions follow
|
||||
//
|
||||
//
|
||||
|
||||
static struct _getopt_data_a {
|
||||
int optind;
|
||||
int opterr;
|
||||
int optopt;
|
||||
char *optarg;
|
||||
int __initialized;
|
||||
char *__nextchar;
|
||||
enum ENUM_ORDERING __ordering;
|
||||
int __first_nonopt;
|
||||
int __last_nonopt;
|
||||
} getopt_data_a;
|
||||
char *optarg_a;
|
||||
|
||||
static void exchange_a(char **argv, struct _getopt_data_a *d) {
|
||||
int bottom = d->__first_nonopt;
|
||||
int middle = d->__last_nonopt;
|
||||
int top = d->optind;
|
||||
char *tem;
|
||||
while (top > middle && middle > bottom) {
|
||||
if (top - middle > middle - bottom) {
|
||||
int len = middle - bottom;
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
tem = argv[bottom + i];
|
||||
argv[bottom + i] = argv[top - (middle - bottom) + i];
|
||||
argv[top - (middle - bottom) + i] = tem;
|
||||
}
|
||||
top -= len;
|
||||
}
|
||||
else {
|
||||
int len = top - middle;
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
tem = argv[bottom + i];
|
||||
argv[bottom + i] = argv[middle + i];
|
||||
argv[middle + i] = tem;
|
||||
}
|
||||
bottom += len;
|
||||
}
|
||||
}
|
||||
d->__first_nonopt += (d->optind - d->__last_nonopt);
|
||||
d->__last_nonopt = d->optind;
|
||||
}
|
||||
|
||||
static int process_long_option_a(int argc, char **argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int print_errors, const char *prefix);
|
||||
static int process_long_option_a(int argc, char **argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int print_errors, const char *prefix) {
|
||||
assert(longopts != NULL);
|
||||
char *nameend;
|
||||
size_t namelen;
|
||||
const struct option_a *p;
|
||||
const struct option_a *pfound = NULL;
|
||||
int n_options;
|
||||
int option_index = 0;
|
||||
for (nameend = d->__nextchar; *nameend && *nameend != '='; nameend++)
|
||||
;
|
||||
namelen = nameend - d->__nextchar;
|
||||
for (p = longopts, n_options = 0; p->name; p++, n_options++)
|
||||
if (!strncmp(p->name, d->__nextchar, namelen) && namelen == strlen(p->name)) {
|
||||
pfound = p;
|
||||
option_index = n_options;
|
||||
break;
|
||||
}
|
||||
if (pfound == NULL) {
|
||||
unsigned char *ambig_set = NULL;
|
||||
int ambig_fallback = 0;
|
||||
int indfound = -1;
|
||||
for (p = longopts, option_index = 0; p->name; p++, option_index++)
|
||||
if (!strncmp(p->name, d->__nextchar, namelen)) {
|
||||
if (pfound == NULL) {
|
||||
pfound = p;
|
||||
indfound = option_index;
|
||||
}
|
||||
else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val) {
|
||||
if (!ambig_fallback) {
|
||||
if (!print_errors)
|
||||
ambig_fallback = 1;
|
||||
|
||||
else if (!ambig_set) {
|
||||
if ((ambig_set = reinterpret_cast<unsigned char *>(malloc(n_options * sizeof(char)))) == NULL)
|
||||
ambig_fallback = 1;
|
||||
|
||||
if (ambig_set) {
|
||||
memset(ambig_set, 0, n_options * sizeof(char));
|
||||
ambig_set[indfound] = 1;
|
||||
}
|
||||
}
|
||||
if (ambig_set)
|
||||
ambig_set[option_index] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ambig_set || ambig_fallback) {
|
||||
if (print_errors) {
|
||||
if (ambig_fallback)
|
||||
fprintf(stderr, "%s: option '%s%s' is ambiguous\n", argv[0], prefix, d->__nextchar);
|
||||
else {
|
||||
_lock_file(stderr);
|
||||
fprintf(stderr, "%s: option '%s%s' is ambiguous; possibilities:", argv[0], prefix, d->__nextchar);
|
||||
for (option_index = 0; option_index < n_options; option_index++)
|
||||
if (ambig_set[option_index])
|
||||
fprintf(stderr, " '%s%s'", prefix, longopts[option_index].name);
|
||||
fprintf(stderr, "\n");
|
||||
_unlock_file(stderr);
|
||||
}
|
||||
}
|
||||
free(ambig_set);
|
||||
d->__nextchar += strlen(d->__nextchar);
|
||||
d->optind++;
|
||||
d->optopt = 0;
|
||||
return '?';
|
||||
}
|
||||
option_index = indfound;
|
||||
}
|
||||
if (pfound == NULL) {
|
||||
if (!long_only || argv[d->optind][1] == '-' || strchr(optstring, *d->__nextchar) == NULL) {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: unrecognized option '%s%s'\n", argv[0], prefix, d->__nextchar);
|
||||
d->__nextchar = NULL;
|
||||
d->optind++;
|
||||
d->optopt = 0;
|
||||
return '?';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
d->optind++;
|
||||
d->__nextchar = NULL;
|
||||
if (*nameend) {
|
||||
if (pfound->has_arg)
|
||||
d->optarg = nameend + 1;
|
||||
else {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: option '%s%s' doesn't allow an argument\n", argv[0], prefix, pfound->name);
|
||||
d->optopt = pfound->val;
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
else if (pfound->has_arg == 1) {
|
||||
if (d->optind < argc)
|
||||
d->optarg = argv[d->optind++];
|
||||
else {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: option '%s%s' requires an argument\n", argv[0], prefix, pfound->name);
|
||||
d->optopt = pfound->val;
|
||||
return optstring[0] == ':' ? ':' : '?';
|
||||
}
|
||||
}
|
||||
if (longind != NULL)
|
||||
*longind = option_index;
|
||||
|
||||
if (pfound->flag) {
|
||||
*(pfound->flag) = pfound->val;
|
||||
return 0;
|
||||
}
|
||||
return pfound->val;
|
||||
}
|
||||
|
||||
static const char *_getopt_initialize_a(const char *optstring, struct _getopt_data_a *d, int posixly_correct) {
|
||||
if (d->optind == 0)
|
||||
d->optind = 1;
|
||||
|
||||
d->__first_nonopt = d->__last_nonopt = d->optind;
|
||||
d->__nextchar = NULL;
|
||||
|
||||
if (optstring[0] == '-') {
|
||||
d->__ordering = RETURN_IN_ORDER;
|
||||
++optstring;
|
||||
}
|
||||
else if (optstring[0] == '+') {
|
||||
d->__ordering = REQUIRE_ORDER;
|
||||
++optstring;
|
||||
}
|
||||
else if (posixly_correct | !!getenv("POSIXLY_CORRECT"))
|
||||
d->__ordering = REQUIRE_ORDER;
|
||||
else
|
||||
d->__ordering = PERMUTE;
|
||||
|
||||
d->__initialized = 1;
|
||||
return optstring;
|
||||
}
|
||||
|
||||
int _getopt_internal_r_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int posixly_correct);
|
||||
int _getopt_internal_r_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int posixly_correct) {
|
||||
int print_errors = d->opterr;
|
||||
if (argc < 1)
|
||||
return -1;
|
||||
d->optarg = NULL;
|
||||
if (d->optind == 0 || !d->__initialized)
|
||||
optstring = _getopt_initialize_a(optstring, d, posixly_correct);
|
||||
else if (optstring[0] == '-' || optstring[0] == '+')
|
||||
optstring++;
|
||||
if (optstring[0] == ':')
|
||||
print_errors = 0;
|
||||
|
||||
if (d->__nextchar == NULL || *d->__nextchar == '\0') {
|
||||
if (d->__last_nonopt > d->optind)
|
||||
d->__last_nonopt = d->optind;
|
||||
if (d->__first_nonopt > d->optind)
|
||||
d->__first_nonopt = d->optind;
|
||||
if (d->__ordering == PERMUTE) {
|
||||
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||
exchange_a(const_cast<char **>(argv), d);
|
||||
else if (d->__last_nonopt != d->optind)
|
||||
d->__first_nonopt = d->optind;
|
||||
while (d->optind < argc && (argv[d->optind][0] != '-' || argv[d->optind][1] == '\0'))
|
||||
d->optind++;
|
||||
d->__last_nonopt = d->optind;
|
||||
}
|
||||
if (d->optind != argc && !strcmp(argv[d->optind], "--")) {
|
||||
d->optind++;
|
||||
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||
exchange_a(const_cast<char **>(argv), d);
|
||||
else if (d->__first_nonopt == d->__last_nonopt)
|
||||
d->__first_nonopt = d->optind;
|
||||
d->__last_nonopt = argc;
|
||||
d->optind = argc;
|
||||
}
|
||||
if (d->optind == argc) {
|
||||
if (d->__first_nonopt != d->__last_nonopt)
|
||||
d->optind = d->__first_nonopt;
|
||||
return -1;
|
||||
}
|
||||
if (argv[d->optind][0] != '-' || argv[d->optind][1] == '\0') {
|
||||
if (d->__ordering == REQUIRE_ORDER)
|
||||
return -1;
|
||||
d->optarg = argv[d->optind++];
|
||||
return 1;
|
||||
}
|
||||
if (longopts) {
|
||||
if (argv[d->optind][1] == '-') {
|
||||
d->__nextchar = argv[d->optind] + 2;
|
||||
return process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts, longind, long_only, d, print_errors, "--");
|
||||
}
|
||||
if (long_only && (argv[d->optind][2] || !strchr(optstring, argv[d->optind][1]))) {
|
||||
int code;
|
||||
d->__nextchar = argv[d->optind] + 1;
|
||||
code = process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts,
|
||||
longind, long_only, d,
|
||||
print_errors, "-");
|
||||
if (code != -1)
|
||||
return code;
|
||||
}
|
||||
}
|
||||
d->__nextchar = argv[d->optind] + 1;
|
||||
}
|
||||
{
|
||||
char c = *d->__nextchar++;
|
||||
const char *temp = strchr(optstring, c);
|
||||
if (*d->__nextchar == '\0')
|
||||
++d->optind;
|
||||
if (temp == NULL || c == ':' || c == ';') {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: invalid option -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
return '?';
|
||||
}
|
||||
if (temp[0] == 'W' && temp[1] == ';' && longopts != NULL) {
|
||||
if (*d->__nextchar != '\0')
|
||||
d->optarg = d->__nextchar;
|
||||
else if (d->optind == argc) {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
if (optstring[0] == ':')
|
||||
c = ':';
|
||||
else
|
||||
c = '?';
|
||||
return c;
|
||||
}
|
||||
else
|
||||
d->optarg = argv[d->optind];
|
||||
d->__nextchar = d->optarg;
|
||||
d->optarg = NULL;
|
||||
return process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts, longind, 0, d, print_errors, "-W ");
|
||||
}
|
||||
if (temp[1] == ':') {
|
||||
if (temp[2] == ':') {
|
||||
if (*d->__nextchar != '\0') {
|
||||
d->optarg = d->__nextchar;
|
||||
d->optind++;
|
||||
}
|
||||
else
|
||||
d->optarg = NULL;
|
||||
d->__nextchar = NULL;
|
||||
}
|
||||
else {
|
||||
if (*d->__nextchar != '\0') {
|
||||
d->optarg = d->__nextchar;
|
||||
d->optind++;
|
||||
}
|
||||
else if (d->optind == argc) {
|
||||
if (print_errors)
|
||||
fprintf(stderr, "%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
if (optstring[0] == ':')
|
||||
c = ':';
|
||||
else
|
||||
c = '?';
|
||||
}
|
||||
else
|
||||
d->optarg = argv[d->optind++];
|
||||
d->__nextchar = NULL;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
int _getopt_internal_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, int posixly_correct);
|
||||
int _getopt_internal_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, int posixly_correct) {
|
||||
int result;
|
||||
getopt_data_a.optind = optind;
|
||||
getopt_data_a.opterr = opterr;
|
||||
result = _getopt_internal_r_a(argc, argv, optstring, longopts, longind, long_only, &getopt_data_a, posixly_correct);
|
||||
optind = getopt_data_a.optind;
|
||||
optarg_a = getopt_data_a.optarg;
|
||||
optopt = getopt_data_a.optopt;
|
||||
return result;
|
||||
}
|
||||
|
||||
int getopt_a(int argc, char *const *argv, const char *optstring) _GETOPT_THROW {
|
||||
return _getopt_internal_a(argc, argv, optstring, static_cast<const struct option_a *>(0), static_cast<int *>(0), 0, 0);
|
||||
}
|
||||
|
||||
int getopt_long_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW {
|
||||
return _getopt_internal_a(argc, argv, options, long_options, opt_index, 0, 0);
|
||||
}
|
||||
|
||||
int getopt_long_only_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW {
|
||||
return _getopt_internal_a(argc, argv, options, long_options, opt_index, 1, 0);
|
||||
}
|
||||
|
||||
int _getopt_long_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d);
|
||||
int _getopt_long_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d) {
|
||||
return _getopt_internal_r_a(argc, argv, options, long_options, opt_index, 0, d, 0);
|
||||
}
|
||||
|
||||
int _getopt_long_only_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d);
|
||||
int _getopt_long_only_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d) {
|
||||
return _getopt_internal_r_a(argc, argv, options, long_options, opt_index, 1, d, 0);
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// Unicode Structures and Functions
|
||||
//
|
||||
//
|
||||
|
||||
static struct _getopt_data_w {
|
||||
int optind;
|
||||
int opterr;
|
||||
int optopt;
|
||||
wchar_t *optarg;
|
||||
int __initialized;
|
||||
wchar_t *__nextchar;
|
||||
enum ENUM_ORDERING __ordering;
|
||||
int __first_nonopt;
|
||||
int __last_nonopt;
|
||||
} getopt_data_w;
|
||||
wchar_t *optarg_w;
|
||||
|
||||
static void exchange_w(wchar_t **argv, struct _getopt_data_w *d) {
|
||||
int bottom = d->__first_nonopt;
|
||||
int middle = d->__last_nonopt;
|
||||
int top = d->optind;
|
||||
wchar_t *tem;
|
||||
while (top > middle && middle > bottom) {
|
||||
if (top - middle > middle - bottom) {
|
||||
int len = middle - bottom;
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
tem = argv[bottom + i];
|
||||
argv[bottom + i] = argv[top - (middle - bottom) + i];
|
||||
argv[top - (middle - bottom) + i] = tem;
|
||||
}
|
||||
top -= len;
|
||||
}
|
||||
else {
|
||||
int len = top - middle;
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
tem = argv[bottom + i];
|
||||
argv[bottom + i] = argv[middle + i];
|
||||
argv[middle + i] = tem;
|
||||
}
|
||||
bottom += len;
|
||||
}
|
||||
}
|
||||
d->__first_nonopt += (d->optind - d->__last_nonopt);
|
||||
d->__last_nonopt = d->optind;
|
||||
}
|
||||
|
||||
static int process_long_option_w(int argc, wchar_t **argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int print_errors, const wchar_t *prefix) {
|
||||
assert(longopts != NULL);
|
||||
wchar_t *nameend;
|
||||
size_t namelen;
|
||||
const struct option_w *p;
|
||||
const struct option_w *pfound = NULL;
|
||||
int n_options;
|
||||
int option_index = 0;
|
||||
for (nameend = d->__nextchar; *nameend && *nameend != L'='; nameend++)
|
||||
;
|
||||
namelen = nameend - d->__nextchar;
|
||||
for (p = longopts, n_options = 0; p->name; p++, n_options++)
|
||||
if (!wcsncmp(p->name, d->__nextchar, namelen) && namelen == wcslen(p->name)) {
|
||||
pfound = p;
|
||||
option_index = n_options;
|
||||
break;
|
||||
}
|
||||
if (pfound == NULL) {
|
||||
wchar_t *ambig_set = NULL;
|
||||
int ambig_fallback = 0;
|
||||
int indfound = -1;
|
||||
for (p = longopts, option_index = 0; p->name; p++, option_index++)
|
||||
if (!wcsncmp(p->name, d->__nextchar, namelen)) {
|
||||
if (pfound == NULL) {
|
||||
pfound = p;
|
||||
indfound = option_index;
|
||||
}
|
||||
else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val) {
|
||||
if (!ambig_fallback) {
|
||||
if (!print_errors)
|
||||
ambig_fallback = 1;
|
||||
|
||||
else if (!ambig_set) {
|
||||
if ((ambig_set = reinterpret_cast<wchar_t *>(malloc(n_options * sizeof(wchar_t)))) == NULL)
|
||||
ambig_fallback = 1;
|
||||
|
||||
if (ambig_set) {
|
||||
memset(ambig_set, 0, n_options * sizeof(wchar_t));
|
||||
ambig_set[indfound] = 1;
|
||||
}
|
||||
}
|
||||
if (ambig_set)
|
||||
ambig_set[option_index] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ambig_set || ambig_fallback) {
|
||||
if (print_errors) {
|
||||
if (ambig_fallback)
|
||||
fwprintf(stderr, L"%s: option '%s%s' is ambiguous\n", argv[0], prefix, d->__nextchar);
|
||||
else {
|
||||
_lock_file(stderr);
|
||||
fwprintf(stderr, L"%s: option '%s%s' is ambiguous; possibilities:", argv[0], prefix, d->__nextchar);
|
||||
for (option_index = 0; option_index < n_options; option_index++)
|
||||
if (ambig_set[option_index])
|
||||
fwprintf(stderr, L" '%s%s'", prefix, longopts[option_index].name);
|
||||
fwprintf(stderr, L"\n");
|
||||
_unlock_file(stderr);
|
||||
}
|
||||
}
|
||||
free(ambig_set);
|
||||
d->__nextchar += wcslen(d->__nextchar);
|
||||
d->optind++;
|
||||
d->optopt = 0;
|
||||
return L'?';
|
||||
}
|
||||
option_index = indfound;
|
||||
}
|
||||
if (pfound == NULL) {
|
||||
if (!long_only || argv[d->optind][1] == L'-' || wcschr(optstring, *d->__nextchar) == NULL) {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: unrecognized option '%s%s'\n", argv[0], prefix, d->__nextchar);
|
||||
d->__nextchar = NULL;
|
||||
d->optind++;
|
||||
d->optopt = 0;
|
||||
return L'?';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
d->optind++;
|
||||
d->__nextchar = NULL;
|
||||
if (*nameend) {
|
||||
if (pfound->has_arg)
|
||||
d->optarg = nameend + 1;
|
||||
else {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: option '%s%s' doesn't allow an argument\n", argv[0], prefix, pfound->name);
|
||||
d->optopt = pfound->val;
|
||||
return L'?';
|
||||
}
|
||||
}
|
||||
else if (pfound->has_arg == 1) {
|
||||
if (d->optind < argc)
|
||||
d->optarg = argv[d->optind++];
|
||||
else {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: option '%s%s' requires an argument\n", argv[0], prefix, pfound->name);
|
||||
d->optopt = pfound->val;
|
||||
return optstring[0] == L':' ? L':' : L'?';
|
||||
}
|
||||
}
|
||||
if (longind != NULL)
|
||||
*longind = option_index;
|
||||
if (pfound->flag) {
|
||||
*(pfound->flag) = pfound->val;
|
||||
return 0;
|
||||
}
|
||||
return pfound->val;
|
||||
}
|
||||
|
||||
static const wchar_t *_getopt_initialize_w(const wchar_t *optstring, struct _getopt_data_w *d, int posixly_correct) {
|
||||
if (d->optind == 0)
|
||||
d->optind = 1;
|
||||
|
||||
d->__first_nonopt = d->__last_nonopt = d->optind;
|
||||
d->__nextchar = NULL;
|
||||
|
||||
if (optstring[0] == L'-') {
|
||||
d->__ordering = RETURN_IN_ORDER;
|
||||
++optstring;
|
||||
}
|
||||
else if (optstring[0] == L'+') {
|
||||
d->__ordering = REQUIRE_ORDER;
|
||||
++optstring;
|
||||
}
|
||||
else if (posixly_correct | !!_wgetenv(L"POSIXLY_CORRECT"))
|
||||
d->__ordering = REQUIRE_ORDER;
|
||||
else
|
||||
d->__ordering = PERMUTE;
|
||||
|
||||
d->__initialized = 1;
|
||||
return optstring;
|
||||
}
|
||||
|
||||
int _getopt_internal_r_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int posixly_correct);
|
||||
int _getopt_internal_r_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int posixly_correct) {
|
||||
int print_errors = d->opterr;
|
||||
if (argc < 1)
|
||||
return -1;
|
||||
d->optarg = NULL;
|
||||
if (d->optind == 0 || !d->__initialized)
|
||||
optstring = _getopt_initialize_w(optstring, d, posixly_correct);
|
||||
else if (optstring[0] == L'-' || optstring[0] == L'+')
|
||||
optstring++;
|
||||
if (optstring[0] == L':')
|
||||
print_errors = 0;
|
||||
#define NONOPTION_P (argv[d->optind][0] != L'-' || argv[d->optind][1] == L'\0')
|
||||
|
||||
if (d->__nextchar == NULL || *d->__nextchar == L'\0') {
|
||||
if (d->__last_nonopt > d->optind)
|
||||
d->__last_nonopt = d->optind;
|
||||
if (d->__first_nonopt > d->optind)
|
||||
d->__first_nonopt = d->optind;
|
||||
if (d->__ordering == PERMUTE) {
|
||||
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||
exchange_w(const_cast<wchar_t **>(argv), d);
|
||||
else if (d->__last_nonopt != d->optind)
|
||||
d->__first_nonopt = d->optind;
|
||||
while (d->optind < argc && NONOPTION_P)
|
||||
d->optind++;
|
||||
d->__last_nonopt = d->optind;
|
||||
}
|
||||
if (d->optind != argc && !wcscmp(argv[d->optind], L"--")) {
|
||||
d->optind++;
|
||||
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||
exchange_w(const_cast<wchar_t **>(argv), d);
|
||||
else if (d->__first_nonopt == d->__last_nonopt)
|
||||
d->__first_nonopt = d->optind;
|
||||
d->__last_nonopt = argc;
|
||||
d->optind = argc;
|
||||
}
|
||||
if (d->optind == argc) {
|
||||
if (d->__first_nonopt != d->__last_nonopt)
|
||||
d->optind = d->__first_nonopt;
|
||||
return -1;
|
||||
}
|
||||
if (NONOPTION_P) {
|
||||
if (d->__ordering == REQUIRE_ORDER)
|
||||
return -1;
|
||||
d->optarg = argv[d->optind++];
|
||||
return 1;
|
||||
}
|
||||
if (longopts) {
|
||||
if (argv[d->optind][1] == L'-') {
|
||||
d->__nextchar = argv[d->optind] + 2;
|
||||
return process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind, long_only, d, print_errors, L"--");
|
||||
}
|
||||
if (long_only && (argv[d->optind][2] || !wcschr(optstring, argv[d->optind][1]))) {
|
||||
int code;
|
||||
d->__nextchar = argv[d->optind] + 1;
|
||||
code = process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind, long_only, d, print_errors, L"-");
|
||||
if (code != -1)
|
||||
return code;
|
||||
}
|
||||
}
|
||||
d->__nextchar = argv[d->optind] + 1;
|
||||
}
|
||||
{
|
||||
wchar_t c = *d->__nextchar++;
|
||||
const wchar_t *temp = wcschr(optstring, c);
|
||||
if (*d->__nextchar == L'\0')
|
||||
++d->optind;
|
||||
if (temp == NULL || c == L':' || c == L';') {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: invalid option -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
return L'?';
|
||||
}
|
||||
if (temp[0] == L'W' && temp[1] == L';' && longopts != NULL) {
|
||||
if (*d->__nextchar != L'\0')
|
||||
d->optarg = d->__nextchar;
|
||||
else if (d->optind == argc) {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
if (optstring[0] == L':')
|
||||
c = L':';
|
||||
else
|
||||
c = L'?';
|
||||
return c;
|
||||
}
|
||||
else
|
||||
d->optarg = argv[d->optind];
|
||||
d->__nextchar = d->optarg;
|
||||
d->optarg = NULL;
|
||||
return process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind,
|
||||
0, d, print_errors, L"-W ");
|
||||
}
|
||||
if (temp[1] == L':') {
|
||||
if (temp[2] == L':') {
|
||||
if (*d->__nextchar != L'\0') {
|
||||
d->optarg = d->__nextchar;
|
||||
d->optind++;
|
||||
}
|
||||
else
|
||||
d->optarg = NULL;
|
||||
d->__nextchar = NULL;
|
||||
}
|
||||
else {
|
||||
if (*d->__nextchar != L'\0') {
|
||||
d->optarg = d->__nextchar;
|
||||
d->optind++;
|
||||
}
|
||||
else if (d->optind == argc) {
|
||||
if (print_errors)
|
||||
fwprintf(stderr, L"%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||
d->optopt = c;
|
||||
if (optstring[0] == L':')
|
||||
c = L':';
|
||||
else
|
||||
c = L'?';
|
||||
}
|
||||
else
|
||||
d->optarg = argv[d->optind++];
|
||||
d->__nextchar = NULL;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
int _getopt_internal_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, int posixly_correct);
|
||||
int _getopt_internal_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, int posixly_correct) {
|
||||
int result;
|
||||
getopt_data_w.optind = optind;
|
||||
getopt_data_w.opterr = opterr;
|
||||
result = _getopt_internal_r_w(argc, argv, optstring, longopts, longind, long_only, &getopt_data_w, posixly_correct);
|
||||
optind = getopt_data_w.optind;
|
||||
optarg_w = getopt_data_w.optarg;
|
||||
optopt = getopt_data_w.optopt;
|
||||
return result;
|
||||
}
|
||||
|
||||
int getopt_w(int argc, wchar_t *const *argv, const wchar_t *optstring) _GETOPT_THROW {
|
||||
return _getopt_internal_w(argc, argv, optstring, static_cast<const struct option_w *>(0), static_cast<int *>(0), 0, 0);
|
||||
}
|
||||
|
||||
int getopt_long_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW {
|
||||
return _getopt_internal_w(argc, argv, options, long_options, opt_index, 0, 0);
|
||||
}
|
||||
|
||||
int getopt_long_only_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW {
|
||||
return _getopt_internal_w(argc, argv, options, long_options, opt_index, 1, 0);
|
||||
}
|
||||
|
||||
int _getopt_long_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d);
|
||||
int _getopt_long_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d) {
|
||||
return _getopt_internal_r_w(argc, argv, options, long_options, opt_index, 0, d, 0);
|
||||
}
|
||||
|
||||
int _getopt_long_only_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d);
|
||||
int _getopt_long_only_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d) {
|
||||
return _getopt_internal_r_w(argc, argv, options, long_options, opt_index, 1, d, 0);
|
||||
}
|
||||
135
3rdparty/getopt/getopt.h
vendored
Normal file
135
3rdparty/getopt/getopt.h
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
/* Getopt for Microsoft C
|
||||
This code is a modification of the Free Software Foundation, Inc.
|
||||
Getopt library for parsing command line argument the purpose was
|
||||
to provide a Microsoft Visual C friendly derivative. This code
|
||||
provides functionality for both Unicode and Multibyte builds.
|
||||
|
||||
Date: 02/03/2011 - Ludvik Jerabek - Initial Release
|
||||
Version: 1.1
|
||||
Comment: Supports getopt, getopt_long, and getopt_long_only
|
||||
and POSIXLY_CORRECT environment flag
|
||||
License: LGPL
|
||||
|
||||
Revisions:
|
||||
|
||||
02/03/2011 - Ludvik Jerabek - Initial Release
|
||||
02/20/2011 - Ludvik Jerabek - Fixed compiler warnings at Level 4
|
||||
07/05/2011 - Ludvik Jerabek - Added no_argument, required_argument, optional_argument defs
|
||||
08/03/2011 - Ludvik Jerabek - Fixed non-argument runtime bug which caused runtime exception
|
||||
08/09/2011 - Ludvik Jerabek - Added code to export functions for DLL and LIB
|
||||
02/15/2012 - Ludvik Jerabek - Fixed _GETOPT_THROW definition missing in implementation file
|
||||
08/01/2012 - Ludvik Jerabek - Created separate functions for char and wchar_t characters so single dll can do both unicode and ansi
|
||||
10/15/2012 - Ludvik Jerabek - Modified to match latest GNU features
|
||||
06/19/2015 - Ludvik Jerabek - Fixed maximum option limitation caused by option_a (255) and option_w (65535) structure val variable
|
||||
09/24/2022 - Ludvik Jerabek - Updated to match most recent getopt release
|
||||
09/25/2022 - Ludvik Jerabek - Fixed memory allocation (malloc call) issue for wchar_t*
|
||||
|
||||
**DISCLAIMER**
|
||||
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
||||
EITHER EXPRESS OR IMPLIED, INCLUDING, BUT Not LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, OR NON-INFRINGEMENT. SOME JURISDICTIONS DO NOT ALLOW THE
|
||||
EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT
|
||||
APPLY TO YOU. IN NO EVENT WILL I BE LIABLE TO ANY PARTY FOR ANY
|
||||
DIRECT, INDIRECT, SPECIAL OR OTHER CONSEQUENTIAL DAMAGES FOR ANY
|
||||
USE OF THIS MATERIAL INCLUDING, WITHOUT LIMITATION, ANY LOST
|
||||
PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR OTHER DATA ON
|
||||
YOUR INFORMATION HANDLING SYSTEM OR OTHERWISE, EVEN If WE ARE
|
||||
EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
*/
|
||||
#ifndef __GETOPT_H_
|
||||
#define __GETOPT_H_
|
||||
|
||||
#ifdef _GETOPT_API
|
||||
# undef _GETOPT_API
|
||||
#endif
|
||||
|
||||
#if defined(EXPORTS_GETOPT) && defined(STATIC_GETOPT)
|
||||
# error "The preprocessor definitions of EXPORTS_GETOPT and STATIC_GETOPT can only be used individually"
|
||||
#elif defined(STATIC_GETOPT)
|
||||
# define _GETOPT_API
|
||||
#elif defined(EXPORTS_GETOPT)
|
||||
# pragma message("Exporting getopt library")
|
||||
# define _GETOPT_API __declspec(dllexport)
|
||||
#else
|
||||
# pragma message("Importing getopt library")
|
||||
# define _GETOPT_API __declspec(dllimport)
|
||||
#endif
|
||||
|
||||
// Change behavior for C\C++
|
||||
#ifdef __cplusplus
|
||||
# define _BEGIN_EXTERN_C extern "C" {
|
||||
# define _END_EXTERN_C }
|
||||
# define _GETOPT_THROW throw()
|
||||
#else
|
||||
# define _BEGIN_EXTERN_C
|
||||
# define _END_EXTERN_C
|
||||
# define _GETOPT_THROW
|
||||
#endif
|
||||
|
||||
// Standard GNU options
|
||||
#define null_argument 0 /*Argument Null*/
|
||||
#define no_argument 0 /*Argument Switch Only*/
|
||||
#define required_argument 1 /*Argument Required*/
|
||||
#define optional_argument 2 /*Argument Optional*/
|
||||
|
||||
// Shorter Options
|
||||
#define ARG_NULL 0 /*Argument Null*/
|
||||
#define ARG_NONE 0 /*Argument Switch Only*/
|
||||
#define ARG_REQ 1 /*Argument Required*/
|
||||
#define ARG_OPT 2 /*Argument Optional*/
|
||||
|
||||
#include <string.h>
|
||||
#include <wchar.h>
|
||||
|
||||
_BEGIN_EXTERN_C
|
||||
|
||||
extern _GETOPT_API int optind;
|
||||
extern _GETOPT_API int opterr;
|
||||
extern _GETOPT_API int optopt;
|
||||
|
||||
// Ansi
|
||||
struct option_a {
|
||||
const char *name;
|
||||
int has_arg;
|
||||
int *flag;
|
||||
int val;
|
||||
};
|
||||
extern _GETOPT_API char *optarg_a;
|
||||
extern _GETOPT_API int getopt_a(int argc, char *const *argv, const char *optstring) _GETOPT_THROW;
|
||||
extern _GETOPT_API int getopt_long_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW;
|
||||
extern _GETOPT_API int getopt_long_only_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW;
|
||||
|
||||
// Unicode
|
||||
struct option_w {
|
||||
const wchar_t *name;
|
||||
int has_arg;
|
||||
int *flag;
|
||||
int val;
|
||||
};
|
||||
extern _GETOPT_API wchar_t *optarg_w;
|
||||
extern _GETOPT_API int getopt_w(int argc, wchar_t *const *argv, const wchar_t *optstring) _GETOPT_THROW;
|
||||
extern _GETOPT_API int getopt_long_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW;
|
||||
extern _GETOPT_API int getopt_long_only_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW;
|
||||
|
||||
_END_EXTERN_C
|
||||
|
||||
#undef _BEGIN_EXTERN_C
|
||||
#undef _END_EXTERN_C
|
||||
#undef _GETOPT_THROW
|
||||
#undef _GETOPT_API
|
||||
|
||||
#ifdef _UNICODE
|
||||
# define getopt getopt_w
|
||||
# define getopt_long getopt_long_w
|
||||
# define getopt_long_only getopt_long_only_w
|
||||
# define option option_w
|
||||
# define optarg optarg_w
|
||||
#else
|
||||
# define getopt getopt_a
|
||||
# define getopt_long getopt_long_a
|
||||
# define getopt_long_only getopt_long_only_a
|
||||
# define option option_a
|
||||
# define optarg optarg_a
|
||||
#endif
|
||||
#endif // __GETOPT_H_
|
||||
3
3rdparty/kdsingleapplication/CMakeLists.txt
vendored
3
3rdparty/kdsingleapplication/CMakeLists.txt
vendored
@@ -3,9 +3,6 @@ set(SOURCES KDSingleApplication/src/kdsingleapplication.cpp KDSingleApplication/
|
||||
set(HEADERS KDSingleApplication/src/kdsingleapplication.h KDSingleApplication/src/kdsingleapplication_localsocket_p.h)
|
||||
qt_wrap_cpp(MOC ${HEADERS})
|
||||
add_library(kdsingleapplication STATIC ${SOURCES} ${MOC})
|
||||
if(NOT MSVC)
|
||||
target_compile_options(kdsingleapplication PRIVATE -Wno-missing-declarations)
|
||||
endif()
|
||||
target_compile_definitions(kdsingleapplication PRIVATE -DKDSINGLEAPPLICATION_STATIC_BUILD)
|
||||
target_include_directories(kdsingleapplication PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(kdsingleapplication PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network)
|
||||
|
||||
1676
CMakeLists.txt
1676
CMakeLists.txt
File diff suppressed because it is too large
Load Diff
96
Changelog
96
Changelog
@@ -2,103 +2,9 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
Version 1.2.7 (2025.01.31):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed strawberry exiting when clicking tray icon.
|
||||
* Fixed Clementine import script errors.
|
||||
* Disabled OSD Pretty on Wayland since it's not working properly.
|
||||
|
||||
Enhancements:
|
||||
* Only maximize error dialog if Strawberry is the active window (#1627).
|
||||
* Added QPA Platform Native Interface as optional component.
|
||||
|
||||
Version 1.2.6 (2025.01.17):
|
||||
|
||||
Bugfixes:
|
||||
* Fixed dragging songs from playlist to queue.
|
||||
|
||||
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:
|
||||
|
||||
* Fixed crash when creating a new smart playlist (#1609).
|
||||
* Fixed last playlist column being added when dragging a song and switching playlists.
|
||||
|
||||
Version 1.2.1 (2024.11.21):
|
||||
|
||||
This release features major restructuring of the codebase, moving source files,
|
||||
rewriting CMake build files, dropping Qt 5 support, external tagreader,
|
||||
and dropping some unmaintained parts such as VLC.
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Fixed playback of CUE continuing to play from the same file after the song has finished playing (#1568).
|
||||
* Fixed updating collection song sort text when disc is changed.
|
||||
* Fixed current playing file left open when the next track errored (#1582).
|
||||
* Fixed filter search not finding song containing uppercase "A" (#1599).
|
||||
* Fixed crash when removing album from playlist when using shuffle albums (#1588).
|
||||
* Fixed IDv3 MBID's tags with multiple entries being ignored.
|
||||
* Fixed crash when enabling Tidal, Spotify, Qobuz or Subsonic services.
|
||||
* Fixed passing filenames to strawberry on command line not resolving to absolute paths.
|
||||
* (macOS) Fixed program not starting for users with long usernames.
|
||||
* (macoS) Fixed crash when pressing caps lock (#1606).
|
||||
* (macOS) Remove "song progress on taskbar" option in behaviour settings.
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Resolve symbolic links when dragging files to the playlist to match collection song.
|
||||
* Replaced Spotify username/password with access token.
|
||||
* Require Qt 6.4 or higher and drop support for Qt 5.
|
||||
* Require TagLib 1.12 or higher.
|
||||
* Use Qt stringliterals.
|
||||
* Move gstfastspectrum to src.
|
||||
* Use standard user temp location for current album cover.
|
||||
* Removed old MacFSListener.
|
||||
* Removed external tagreader and protobuf dependency.
|
||||
* Removed VLC support.
|
||||
* Ported to Qt translation (.ts) files and removed gettext dependency.
|
||||
* Removed deprecated Gnome/Mate SettingsDaemon global shortcuts.
|
||||
|
||||
Version 1.1.3 (2024.09.21):
|
||||
|
||||
Bugfixes:
|
||||
Bugfixes:
|
||||
* Fixed gstreamer registry lookup leak in Spotify settings.
|
||||
* Fixed all songs in a CUE sheet starting playback at the zero position (#1549).
|
||||
* Fixed playback going to pause and back to play on song change.
|
||||
|
||||
19
README.md
19
README.md
@@ -33,8 +33,8 @@ Resources:
|
||||
The program is free software, released under GPL. If you like this program and can make use of it, consider sponsoring or donating to help fund the project.
|
||||
There are currently 4 options for sponsoring:
|
||||
|
||||
1. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||
2. [GitHub](https://github.com/sponsors/jonaski)
|
||||
1. [GitHub](https://github.com/sponsors/jonaski)
|
||||
2. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
||||
4. [PayPal](https://paypal.me/jonaskvinge)
|
||||
|
||||
@@ -75,13 +75,14 @@ To build Strawberry from source you need the following installed on your system
|
||||
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
||||
* [Boost](https://www.boost.org/)
|
||||
* [GLib](https://developer.gnome.org/glib/)
|
||||
* [Qt 6.4.0 or higher with components Core, Concurrent, Gui, Widgets, Network, Sql and D-Bus](https://www.qt.io/)
|
||||
* [Qt 6 or Qt 5.12 or higher with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/)
|
||||
* [SQLite 3.9 or newer](https://www.sqlite.org)
|
||||
* [Protobuf](https://developers.google.com/protocol-buffers/)
|
||||
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
|
||||
* [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
* [TagLib 1.12 or higher](https://www.taglib.org/)
|
||||
* [D-Bus (Required on Linux)](https://www.freedesktop.org/wiki/Software/dbus/)
|
||||
* [GStreamer](https://gstreamer.freedesktop.org/) or [VLC](https://www.videolan.org)
|
||||
* [TagLib 1.11.1 or higher](https://www.taglib.org/) or [TagParser](https://github.com/Martchus/tagparser)
|
||||
* [ICU](https://unicode-org.github.io/icu/)
|
||||
* [KDSingleApplication](https://github.com/KDAB/KDSingleApplication)
|
||||
|
||||
Optional dependencies:
|
||||
|
||||
@@ -106,10 +107,14 @@ You should also install the gstreamer plugins base and good, and optionally bad,
|
||||
cd strawberry
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
cmake .. -DBUILD_WITH_QT6=ON
|
||||
make -j $(nproc)
|
||||
sudo make install
|
||||
|
||||
Strawberry is backwards compatible with Qt 5, to compile with Qt 5 use:
|
||||
|
||||
cmake .. -DBUILD_WITH_QT5=ON
|
||||
|
||||
To compile on Windows with Visual Studio 2019 or 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||
|
||||
### :penguin: Packaging status
|
||||
|
||||
@@ -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 -nc -j4)
|
||||
add_custom_target(deb WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${DPKG_BUILDPACKAGE} -b -d -uc -us)
|
||||
endif()
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin /usr/local/opt/qt5/bin REQUIRED)
|
||||
if(MACDEPLOYQT_EXECUTABLE)
|
||||
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing macdeployqt executable.")
|
||||
endif()
|
||||
|
||||
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin REQUIRED)
|
||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||
message(STATUS "Found macdeploycheck: ${MACDEPLOYCHECK_EXECUTABLE}")
|
||||
else()
|
||||
message(WARNING "Missing macdeploycheck executable.")
|
||||
endif()
|
||||
|
||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg REQUIRED)
|
||||
if(CREATEDMG_EXECUTABLE)
|
||||
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
||||
@@ -31,18 +24,17 @@ if(MACDEPLOYQT_EXECUTABLE)
|
||||
|
||||
add_custom_target(deploy
|
||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/{Frameworks,Resources}
|
||||
COMMAND cp -v ${CMAKE_BINARY_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
|
||||
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/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}
|
||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
DEPENDS strawberry
|
||||
DEPENDS strawberry strawberry-tagreader
|
||||
)
|
||||
add_custom_target(deploycheck
|
||||
COMMAND ${CMAKE_BINARY_DIR}/ext/macdeploycheck/macdeploycheck strawberry.app
|
||||
DEPENDS macdeploycheck
|
||||
)
|
||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
||||
add_custom_target(deploycheck
|
||||
COMMAND ${MACDEPLOYCHECK_EXECUTABLE} strawberry.app
|
||||
)
|
||||
endif()
|
||||
if(CREATEDMG_EXECUTABLE)
|
||||
add_custom_target(dmg
|
||||
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_CODESIGN} ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
||||
|
||||
69
cmake/FindCppUnit.cmake
Normal file
69
cmake/FindCppUnit.cmake
Normal file
@@ -0,0 +1,69 @@
|
||||
# - Try to find the libcppunit libraries
|
||||
# Once done this will define
|
||||
#
|
||||
# CppUnit_FOUND - system has libcppunit
|
||||
# CPPUNIT_INCLUDE_DIR - the libcppunit include directory
|
||||
# CPPUNIT_LIBRARIES - libcppunit library
|
||||
|
||||
#include (MacroEnsureVersion)
|
||||
|
||||
if(NOT CPPUNIT_MIN_VERSION)
|
||||
SET(CPPUNIT_MIN_VERSION 1.12.0)
|
||||
endif(NOT CPPUNIT_MIN_VERSION)
|
||||
|
||||
FIND_PROGRAM(CPPUNIT_CONFIG_EXECUTABLE cppunit-config )
|
||||
|
||||
IF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
# in cache already
|
||||
SET(CppUnit_FOUND TRUE)
|
||||
|
||||
ELSE(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
SET(CPPUNIT_INCLUDE_DIR)
|
||||
SET(CPPUNIT_LIBRARIES)
|
||||
|
||||
IF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --cflags RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_CFLAGS)
|
||||
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --libs RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_LIBRARIES)
|
||||
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_INSTALLED_VERSION)
|
||||
STRING(REGEX REPLACE "-I(.+)" "\\1" CPPUNIT_CFLAGS "${CPPUNIT_CFLAGS}")
|
||||
ELSE(CPPUNIT_CONFIG_EXECUTABLE)
|
||||
# in case win32 needs to find it the old way?
|
||||
FIND_PATH(CPPUNIT_CFLAGS cppunit/TestRunner.h PATHS /usr/include /usr/local/include )
|
||||
FIND_LIBRARY(CPPUNIT_LIBRARIES NAMES cppunit PATHS /usr/lib /usr/local/lib )
|
||||
# how can we find cppunit version?
|
||||
MESSAGE (STATUS "Ensure you cppunit installed version is at least ${CPPUNIT_MIN_VERSION}")
|
||||
SET (CPPUNIT_INSTALLED_VERSION ${CPPUNIT_MIN_VERSION})
|
||||
ENDIF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||
|
||||
SET(CPPUNIT_INCLUDE_DIR ${CPPUNIT_CFLAGS} "${CPPUNIT_CFLAGS}/cppunit")
|
||||
|
||||
ENDIF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
IF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
SET(CppUnit_FOUND TRUE)
|
||||
|
||||
if(NOT CppUnit_FIND_QUIETLY)
|
||||
MESSAGE (STATUS "Found cppunit: ${CPPUNIT_LIBRARIES}")
|
||||
endif(NOT CppUnit_FIND_QUIETLY)
|
||||
|
||||
IF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_INSTALLED_VERSION)
|
||||
ENDIF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||
|
||||
#macro_ensure_version( ${CPPUNIT_MIN_VERSION} ${CPPUNIT_INSTALLED_VERSION} CPPUNIT_INSTALLED_VERSION_OK )
|
||||
|
||||
#IF(NOT CPPUNIT_INSTALLED_VERSION_OK)
|
||||
# MESSAGE ("** CppUnit version is too old: found ${CPPUNIT_INSTALLED_VERSION} installed, ${CPPUNIT_MIN_VERSION} or major is required")
|
||||
# SET(CppUnit_FOUND FALSE)
|
||||
#ENDIF(NOT CPPUNIT_INSTALLED_VERSION_OK)
|
||||
|
||||
ELSE(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
SET(CppUnit_FOUND FALSE CACHE BOOL "Not found cppunit library")
|
||||
|
||||
ENDIF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||
|
||||
MARK_AS_ADVANCED(CPPUNIT_INCLUDE_DIR CPPUNIT_LIBRARIES)
|
||||
132
cmake/FindFFTW3.cmake
Normal file
132
cmake/FindFFTW3.cmake
Normal file
@@ -0,0 +1,132 @@
|
||||
#
|
||||
# Try to find FFTW3 library
|
||||
# (see www.fftw.org)
|
||||
# Once run this will define:
|
||||
#
|
||||
# FFTW3_FOUND
|
||||
# FFTW3_INCLUDE_DIR
|
||||
# FFTW3_LIBRARIES
|
||||
# FFTW3_LINK_DIRECTORIES
|
||||
#
|
||||
# You may set one of these options before including this file:
|
||||
# FFTW3_USE_SSE2
|
||||
#
|
||||
# TODO: _F_ versions.
|
||||
#
|
||||
# Jan Woetzel 05/2004
|
||||
# www.mip.informatik.uni-kiel.de
|
||||
# --------------------------------
|
||||
|
||||
FIND_PATH(FFTW3_INCLUDE_DIR fftw3.h
|
||||
${FFTW3_DIR}/include
|
||||
${FFTW3_HOME}/include
|
||||
${FFTW3_DIR}
|
||||
${FFTW3_HOME}
|
||||
$ENV{FFTW3_DIR}/include
|
||||
$ENV{FFTW3_HOME}/include
|
||||
$ENV{FFTW3_DIR}
|
||||
$ENV{FFTW3_HOME}
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
$ENV{SOURCE_DIR}/fftw3
|
||||
$ENV{SOURCE_DIR}/fftw3/include
|
||||
$ENV{SOURCE_DIR}/fftw
|
||||
$ENV{SOURCE_DIR}/fftw/include
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_INCLUDE_DIR=${FFTW3_INCLUDE_DIR}")
|
||||
|
||||
|
||||
SET(FFTW3_POSSIBLE_LIBRARY_PATH
|
||||
${FFTW3_DIR}/lib
|
||||
${FFTW3_HOME}/lib
|
||||
${FFTW3_DIR}
|
||||
${FFTW3_HOME}
|
||||
$ENV{FFTW3_DIR}/lib
|
||||
$ENV{FFTW3_HOME}/lib
|
||||
$ENV{FFTW3_DIR}
|
||||
$ENV{FFTW3_HOME}
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
$ENV{SOURCE_DIR}/fftw3
|
||||
$ENV{SOURCE_DIR}/fftw3/lib
|
||||
$ENV{SOURCE_DIR}/fftw
|
||||
$ENV{SOURCE_DIR}/fftw/lib
|
||||
)
|
||||
|
||||
|
||||
# The lib prefix is contained in filename of W32, unfortunately. In the "general" lib:
|
||||
FIND_LIBRARY(FFTW3_FFTW_LIBRARY
|
||||
NAMES fftw3 libfftw libfftw3 libfftw3-3
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTW_LIBRARY=${FFTW3_FFTW_LIBRARY}")
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTWF_LIBRARY
|
||||
NAMES fftwf3 fftw3f fftwf libfftwf libfftwf3 libfftw3f-3
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTWF_LIBRARY=${FFTW3_FFTWF_LIBRARY}")
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTWL_LIBRARY
|
||||
NAMES fftwl3 fftw3l fftwl libfftwl libfftwl3 libfftw3l-3
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTWF_LIBRARY=${FFTW3_FFTWL_LIBRARY}")
|
||||
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTW_SSE2_LIBRARY
|
||||
NAMES fftw_sse2 fftw3_sse2 libfftw_sse2 libfftw3_sse2
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTW_SSE2_LIBRARY=${FFTW3_FFTW_SSE2_LIBRARY}")
|
||||
|
||||
FIND_LIBRARY(FFTW3_FFTWF_SSE_LIBRARY
|
||||
NAMES fftwf_sse fftwf3_sse libfftwf_sse libfftwf3_sse
|
||||
PATHS
|
||||
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||
)
|
||||
#MESSAGE("DBG FFTW3_FFTWF_SSE_LIBRARY=${FFTW3_FFTWF_SSE_LIBRARY}")
|
||||
|
||||
|
||||
# --------------------------------
|
||||
# select one of the above
|
||||
# default:
|
||||
IF (FFTW3_FFTW_LIBRARY)
|
||||
SET(FFTW3_LIBRARIES ${FFTW3_FFTW_LIBRARY})
|
||||
ENDIF (FFTW3_FFTW_LIBRARY)
|
||||
# specialized:
|
||||
IF (FFTW3_USE_SSE2 AND FFTW3_FFTW_SSE2_LIBRARY)
|
||||
SET(FFTW3_LIBRARIES ${FFTW3_FFTW_SSE2_LIBRARY})
|
||||
ENDIF (FFTW3_USE_SSE2 AND FFTW3_FFTW_SSE2_LIBRARY)
|
||||
|
||||
# --------------------------------
|
||||
|
||||
IF(FFTW3_LIBRARIES)
|
||||
IF (FFTW3_INCLUDE_DIR)
|
||||
|
||||
# OK, found all we need
|
||||
SET(FFTW3_FOUND TRUE)
|
||||
GET_FILENAME_COMPONENT(FFTW3_LINK_DIRECTORIES ${FFTW3_LIBRARIES} PATH)
|
||||
|
||||
ELSE (FFTW3_INCLUDE_DIR)
|
||||
MESSAGE("FFTW3 include dir not found. Set FFTW3_DIR to find it.")
|
||||
ENDIF(FFTW3_INCLUDE_DIR)
|
||||
ELSE(FFTW3_LIBRARIES)
|
||||
MESSAGE("FFTW3 lib not found. Set FFTW3_DIR to find it.")
|
||||
ENDIF(FFTW3_LIBRARIES)
|
||||
|
||||
|
||||
MARK_AS_ADVANCED(
|
||||
FFTW3_INCLUDE_DIR
|
||||
FFTW3_LIBRARIES
|
||||
FFTW3_FFTW_LIBRARY
|
||||
FFTW3_FFTW_SSE2_LIBRARY
|
||||
FFTW3_FFTWF_LIBRARY
|
||||
FFTW3_FFTWF_SSE_LIBRARY
|
||||
FFTW3_FFTWL_LIBRARY
|
||||
FFTW3_LINK_DIRECTORIES
|
||||
)
|
||||
@@ -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_BINARY_DIR}/strawberry.spec
|
||||
COMMAND ${RPMBUILD_EXEC} -ba ${CMAKE_SOURCE_DIR}/dist/unix/strawberry.spec
|
||||
)
|
||||
|
||||
endif()
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
set(summary_willbuild "")
|
||||
set(summary_willnotbuild "")
|
||||
|
||||
macro(optional_component_summary_add name test)
|
||||
macro(summary_add name test)
|
||||
if (${test})
|
||||
list(APPEND summary_willbuild ${name})
|
||||
else (${test})
|
||||
list(APPEND summary_willnotbuild "${name}")
|
||||
endif (${test})
|
||||
endmacro(optional_component_summary_add)
|
||||
endmacro(summary_add)
|
||||
|
||||
macro(optional_component_summary_show_part variable title)
|
||||
macro(summary_show_part variable title)
|
||||
list(LENGTH ${variable} _len)
|
||||
if (_len)
|
||||
message("")
|
||||
@@ -18,20 +18,19 @@ macro(optional_component_summary_show_part variable title)
|
||||
message(" ${_item}")
|
||||
endforeach (_item)
|
||||
endif (_len)
|
||||
endmacro(optional_component_summary_show_part)
|
||||
endmacro(summary_show_part)
|
||||
|
||||
macro(optional_component_summary_show)
|
||||
macro(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}")
|
||||
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:")
|
||||
summary_show_part(summary_willbuild "The following components will be built:")
|
||||
summary_show_part(summary_willnotbuild "The following components WILL NOT be built:")
|
||||
message("")
|
||||
endmacro(optional_component_summary_show)
|
||||
endmacro(summary_show)
|
||||
|
||||
function(optional_component name default description)
|
||||
|
||||
set(option_variable "ENABLE_${name}")
|
||||
set(have_variable "HAVE_${name}")
|
||||
set(${have_variable} OFF)
|
||||
@@ -80,9 +79,6 @@ 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)
|
||||
92
cmake/Translations.cmake
Normal file
92
cmake/Translations.cmake
Normal file
@@ -0,0 +1,92 @@
|
||||
find_program(GETTEXT_XGETTEXT_EXECUTABLE xgettext REQUIRED)
|
||||
find_program(CAT_EXECUTABLE cat REQUIRED)
|
||||
|
||||
list(APPEND XGETTEXT_OPTIONS
|
||||
--qt
|
||||
--keyword=tr:1,2c
|
||||
--keyword=tr
|
||||
--flag=tr:1:pass-c-format
|
||||
--flag=tr:1:pass-qt-format
|
||||
--keyword=trUtf8
|
||||
--flag=tr:1:pass-c-format
|
||||
--flag=tr:1:pass-qt-format
|
||||
--keyword=translate:2,3c
|
||||
--keyword=translate:2
|
||||
--flag=translate:2:pass-c-format
|
||||
--flag=translate:2:pass-qt-format
|
||||
--keyword=QT_TR_NOOP
|
||||
--flag=QT_TR_NOOP:1:pass-c-format
|
||||
--flag=QT_TR_NOOP:1:pass-qt-format
|
||||
--keyword=QT_TRANSLATE_NOOP:2
|
||||
--flag=QT_TRANSLATE_NOOP:2:pass-c-format
|
||||
--flag=QT_TRANSLATE_NOOP:2:pass-qt-format
|
||||
--keyword=_
|
||||
--flag=_:1:pass-c-format
|
||||
--flag=_:1:pass-qt-format
|
||||
--keyword=N_
|
||||
--flag=N_:1:pass-c-format
|
||||
--flag=N_:1:pass-qt-format
|
||||
--from-code=utf-8
|
||||
)
|
||||
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/translations)
|
||||
|
||||
macro(add_pot outfiles header pot)
|
||||
# Make relative filenames for all source files
|
||||
set(add_pot_sources)
|
||||
foreach(_filename ${ARGN})
|
||||
get_filename_component(_absolute_filename ${_filename} ABSOLUTE)
|
||||
file(RELATIVE_PATH _relative_filename ${CMAKE_CURRENT_SOURCE_DIR} ${_absolute_filename})
|
||||
list(APPEND add_pot_sources ${_relative_filename})
|
||||
endforeach(_filename)
|
||||
|
||||
# Generate the .pot
|
||||
add_custom_command(
|
||||
OUTPUT ${pot}
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMAND ${GETTEXT_XGETTEXT_EXECUTABLE} ${XGETTEXT_OPTIONS} -C --omit-header --no-location --output="${CMAKE_CURRENT_BINARY_DIR}/pot.temp" ${add_pot_sources}
|
||||
COMMAND cat ${header} ${CMAKE_CURRENT_BINARY_DIR}/pot.temp > ${pot}
|
||||
DEPENDS ${add_pot_sources} ${header}
|
||||
)
|
||||
|
||||
list(APPEND ${outfiles} ${pot})
|
||||
endmacro(add_pot)
|
||||
|
||||
# Syntax is:
|
||||
# add_po(sources_var po_prefix LANGUAGES language1 language2 ... DIRECTORY dir)
|
||||
|
||||
macro(add_po outfiles po_prefix)
|
||||
parse_arguments(ADD_PO
|
||||
"LANGUAGES;DIRECTORY"
|
||||
""
|
||||
${ARGN}
|
||||
)
|
||||
|
||||
foreach (_lang ${ADD_PO_LANGUAGES})
|
||||
set(_po_filename "${_lang}.po")
|
||||
set(_po_filepath "${CMAKE_CURRENT_SOURCE_DIR}/${ADD_PO_DIRECTORY}/${_po_filename}")
|
||||
set(_qm_filename "strawberry_${_lang}.qm")
|
||||
set(_qm_filepath "${CMAKE_CURRENT_BINARY_DIR}/${ADD_PO_DIRECTORY}/${_qm_filename}")
|
||||
|
||||
# Convert the .po files to .qm files
|
||||
add_custom_command(
|
||||
OUTPUT ${_qm_filepath}
|
||||
COMMAND ${QT_LCONVERT_EXECUTABLE} ARGS ${_po_filepath} -o ${_qm_filepath} -of qm -target-language ${_lang}
|
||||
DEPENDS ${_po_filepath} ${_po_filepath}
|
||||
)
|
||||
|
||||
list(APPEND ${outfiles} ${_qm_filepath})
|
||||
list(APPEND INSTALL_TRANSLATIONS_FILES ${_qm_filepath})
|
||||
endforeach (_lang)
|
||||
|
||||
# Generate a qrc file for the translations
|
||||
if(NOT INSTALL_TRANSLATIONS)
|
||||
set(_qrc ${CMAKE_CURRENT_BINARY_DIR}/${ADD_PO_DIRECTORY}/translations.qrc)
|
||||
file(WRITE ${_qrc} "<RCC><qresource prefix=\"/${ADD_PO_DIRECTORY}\">")
|
||||
foreach(_lang ${ADD_PO_LANGUAGES})
|
||||
file(APPEND ${_qrc} "<file>${po_prefix}${_lang}.qm</file>")
|
||||
endforeach(_lang)
|
||||
file(APPEND ${_qrc} "</qresource></RCC>")
|
||||
qt_add_resources(${outfiles} ${_qrc})
|
||||
endif()
|
||||
endmacro(add_po)
|
||||
@@ -1,9 +1,9 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 2)
|
||||
set(STRAWBERRY_VERSION_PATCH 7)
|
||||
set(STRAWBERRY_VERSION_MINOR 1)
|
||||
set(STRAWBERRY_VERSION_PATCH 3)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
set(INCLUDE_GIT_REVISION ON)
|
||||
|
||||
set(majorminorpatch "${STRAWBERRY_VERSION_MAJOR}.${STRAWBERRY_VERSION_MINOR}.${STRAWBERRY_VERSION_PATCH}")
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
files:
|
||||
- source: /src/translations/strawberry_en_US.ts
|
||||
translation: /src/translations/strawberry_%locale_with_underscore%.ts
|
||||
- source: /src/translations/translations.pot
|
||||
translation: /src/translations/%locale_with_underscore%.po
|
||||
|
||||
12
debian/CMakeLists.txt
vendored
12
debian/CMakeLists.txt
vendored
@@ -4,7 +4,19 @@ if(LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
|
||||
execute_process(COMMAND /bin/sh "-c" "${LSB_RELEASE_EXEC} -cs" OUTPUT_VARIABLE DEB_CODENAME OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
if(DEB_CODENAME AND DEB_DATE)
|
||||
|
||||
if(QT_VERSION_MAJOR EQUAL 5)
|
||||
set(DEBIAN_BUILD_DEPENDS_QT_PACKAGES qtbase5-dev,qtbase5-dev-tools,qttools5-dev,qttools5-dev-tools,libqt5x11extras5-dev)
|
||||
set(DEBIAN_DEPENDS_QT_PACKAGES libqt5sql5-sqlite)
|
||||
endif()
|
||||
if(QT_VERSION_MAJOR EQUAL 6)
|
||||
set(DEBIAN_BUILD_DEPENDS_QT_PACKAGES qt6-base-dev,qt6-base-dev-tools,qt6-tools-dev,qt6-tools-dev-tools,qt6-l10n-tools)
|
||||
set(DEBIAN_DEPENDS_QT_PACKAGES libqt6sql6-sqlite,qt6-qpa-plugins)
|
||||
endif()
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/control.in ${CMAKE_CURRENT_SOURCE_DIR}/control @ONLY)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/changelog.in ${CMAKE_CURRENT_SOURCE_DIR}/changelog)
|
||||
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
||||
3
debian/clean
vendored
3
debian/clean
vendored
@@ -1,3 +0,0 @@
|
||||
dist/scripts/maketarball.sh
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
1
debian/compat
vendored
Normal file
1
debian/compat
vendored
Normal file
@@ -0,0 +1 @@
|
||||
11
|
||||
18
debian/control → debian/control.in
vendored
18
debian/control → debian/control.in
vendored
@@ -2,26 +2,23 @@ Source: strawberry
|
||||
Section: sound
|
||||
Priority: optional
|
||||
Maintainer: Jonas Kvinge <jonas@jkvinge.net>
|
||||
Build-Depends: debhelper-compat (= 12),
|
||||
Build-Depends: debhelper (>= 11),
|
||||
git,
|
||||
make,
|
||||
cmake,
|
||||
gcc,
|
||||
g++,
|
||||
protobuf-compiler,
|
||||
libglib2.0-dev,
|
||||
libdbus-1-dev,
|
||||
libprotobuf-dev,
|
||||
libboost-dev,
|
||||
libsqlite3-dev,
|
||||
libasound2-dev,
|
||||
libpulse-dev,
|
||||
libtag1-dev,
|
||||
libicu-dev,
|
||||
libxkbcommon-dev,
|
||||
qt6-base-dev,
|
||||
qt6-base-private-dev,
|
||||
qt6-base-dev-tools,
|
||||
qt6-tools-dev,
|
||||
qt6-tools-dev-tools,
|
||||
qt6-l10n-tools,
|
||||
@DEBIAN_BUILD_DEPENDS_QT_PACKAGES@,
|
||||
libgstreamer1.0-dev,
|
||||
libgstreamer-plugins-base1.0-dev,
|
||||
libcdio-dev,
|
||||
@@ -30,14 +27,13 @@ Build-Depends: debhelper-compat (= 12),
|
||||
libchromaprint-dev,
|
||||
libfftw3-dev,
|
||||
libebur128-dev
|
||||
Standards-Version: 4.7.0
|
||||
Standards-Version: 4.6.1
|
||||
|
||||
Package: strawberry
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends},
|
||||
${misc:Depends},
|
||||
libqt6sql6-sqlite,
|
||||
qt6-qpa-plugins,
|
||||
@DEBIAN_DEPENDS_QT_PACKAGES@,
|
||||
gstreamer1.0-plugins-base,
|
||||
gstreamer1.0-plugins-good,
|
||||
gstreamer1.0-alsa,
|
||||
61
debian/copyright
vendored
61
debian/copyright
vendored
@@ -5,12 +5,14 @@ Source: https://github.com/strawberrymusicplayer/strawberry
|
||||
|
||||
Files: *
|
||||
Copyright: 2010-2015, David Sansome <me@davidsansome.com>
|
||||
2012-2014, 2017-2024 Jonas Kvinge <jonas@jkvinge.net>
|
||||
2012-2014, 2017-2023 Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/utilities/timeconstants.h
|
||||
src/core/logging.cpp
|
||||
src/core/logging.h
|
||||
ext/libstrawberry-common/core/logging.cpp
|
||||
ext/libstrawberry-common/core/logging.h
|
||||
ext/libstrawberry-common/core/messagehandler.cpp
|
||||
ext/libstrawberry-common/core/messagehandler.h
|
||||
Copyright: 2011, 2012, David Sansome <me@davidsansome.com>
|
||||
2018-2022, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: Apache-2.0
|
||||
@@ -29,20 +31,18 @@ Files: src/core/main.h
|
||||
src/engine/alsapcmdevicefinder.h
|
||||
src/engine/mmdevicefinder.cpp
|
||||
src/engine/mmdevicefinder.h
|
||||
src/engine/uwpdevicefinder.cpp
|
||||
src/engine/uwpdevicefinder.h
|
||||
src/engine/devicefinder.cpp
|
||||
src/engine/devicefinder.h
|
||||
src/engine/enginedevice.cpp
|
||||
src/engine/enginedevice.h
|
||||
src/engine/enginemetadata.cpp
|
||||
src/engine/enginemetadata.h
|
||||
src/streaming/streamingservice.cpp
|
||||
src/streaming/streamingservice.h
|
||||
src/streaming/streamingtabsview.cpp
|
||||
src/streaming/streamingtabsview.h
|
||||
src/streaming/streamingsongsview.cpp
|
||||
src/streaming/streamingsongsview.h
|
||||
src/internet/internetservice.cpp
|
||||
src/internet/internetservice.h
|
||||
src/internet/internettabsview.cpp
|
||||
src/internet/internettabsview.h
|
||||
src/internet/internetsongsview.cpp
|
||||
src/internet/internetsongsview.h
|
||||
src/settings/backendsettingspage.cpp
|
||||
src/settings/backendsettingspage.h
|
||||
src/settings/coverssettingspage.cpp
|
||||
@@ -55,8 +55,6 @@ Files: src/core/main.h
|
||||
src/settings/subsonicsettingspage.h
|
||||
src/settings/tidalsettingspage.cpp
|
||||
src/settings/tidalsettingspage.h
|
||||
src/settings/spotifysettingspage.cpp
|
||||
src/settings/spotifysettingspage.h
|
||||
src/covermanager/jsoncoverprovider.cpp
|
||||
src/covermanager/jsoncoverprovider.h
|
||||
src/covermanager/lastfmcoverprovider.cpp
|
||||
@@ -67,16 +65,16 @@ Files: src/core/main.h
|
||||
src/covermanager/deezercoverprovider.h
|
||||
src/covermanager/tidalcoverprovider.cpp
|
||||
src/covermanager/tidalcoverprovider.h
|
||||
src/covermanager/opentidalcoverprovider.cpp
|
||||
src/covermanager/opentidalcoverprovider.h
|
||||
src/covermanager/qobuzcoverprovider.cpp
|
||||
src/covermanager/qobuzcoverprovider.h
|
||||
src/covermanager/spotifycoverprovider.cpp
|
||||
src/covermanager/spotifycoverprovider.h
|
||||
src/covermanager/musixmatchcoverprovider.cpp
|
||||
src/covermanager/musixmatchcoverprovider.h
|
||||
src/globalshortcuts/globalshortcutsbackend-kglobalaccel.cpp
|
||||
src/globalshortcuts/globalshortcutsbackend-kglobalaccel.h
|
||||
src/globalshortcuts/globalshortcutsbackend-kde.cpp
|
||||
src/globalshortcuts/globalshortcutsbackend-kde.h
|
||||
src/globalshortcuts/globalshortcutsbackend-mate.cpp
|
||||
src/globalshortcuts/globalshortcutsbackend-mate.h
|
||||
src/globalshortcuts/globalshortcutsbackend-x11.cpp
|
||||
src/globalshortcuts/globalshortcutsbackend-x11.h
|
||||
src/globalshortcuts/globalshortcutsbackend-win.cpp
|
||||
@@ -93,14 +91,14 @@ Files: src/core/main.h
|
||||
src/tidal/*
|
||||
src/qobuz/*
|
||||
src/radios/*
|
||||
src/spotify/*
|
||||
src/transcoder/transcoderoptionswavpack.cpp
|
||||
src/transcoder/transcoderoptionswavpack.h
|
||||
ext/libstrawberry-tagreader/tagreadertagparser.cpp
|
||||
ext/libstrawberry-tagreader/tagreadertagparser.h
|
||||
ext/macdeploycheck/*
|
||||
src/widgets/resizabletextedit.cpp
|
||||
src/widgets/resizabletextedit.h
|
||||
src/widgets/fancytabdata.cpp
|
||||
src/widgets/fancytabdata.h
|
||||
Copyright: 2012-2014, 2017-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
Copyright: 2012-2014, 2017-2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/engine/enginebase.cpp
|
||||
@@ -121,8 +119,6 @@ License: GPL-2+
|
||||
|
||||
Files: src/widgets/fancytabwidget.cpp
|
||||
src/widgets/fancytabwidget.h
|
||||
src/widgets/fancytabbar.cpp
|
||||
src/widgets/fancytabbar.h
|
||||
Copyright: 2018, Vikram Ambrose <ambroseworks@gmail.com>
|
||||
2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-3+
|
||||
@@ -212,8 +208,8 @@ Files: src/device/udisks2lister.cpp
|
||||
Copyright: 2016, Valeriy Malov <jazzvoid@gmail.com>
|
||||
License: GPL-3+
|
||||
|
||||
Files: src/core/localredirectserver.cpp
|
||||
src/core/localredirectserver.h
|
||||
Files: src/internet/localredirectserver.cpp
|
||||
src/internet/localredirectserver.h
|
||||
Copyright: 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
@@ -252,24 +248,25 @@ Files: src/core/stylehelper.cpp
|
||||
Copyright: 2016 The Qt Company Ltd.
|
||||
License: GPL-3+
|
||||
|
||||
Files: 3rdparty/gstfastspectrum/gstfastspectrum.cpp
|
||||
3rdparty/gstfastspectrum/gstfastspectrum.h
|
||||
Files: ext/gstmoodbar/gstfastspectrum.cpp
|
||||
ext/gstmoodbar/gstfastspectrum.h
|
||||
Copyright: 1999 Erik Walthinsen <omega@cse.ogi.edu>
|
||||
2006,2011 Stefan Kost <ensonic@users.sf.net>
|
||||
2007-2009 Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
2018-2024 Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: GPL-2+
|
||||
|
||||
Files: src/widgets/qsearchfield_qt.cpp
|
||||
Files: src/widgets/qsearchfield_nonmac.cpp
|
||||
src/widgets/qsearchfield_mac.mm
|
||||
src/widgets/qsearchfield.h
|
||||
src/widgets/qocoa_mac.h
|
||||
src/widgets/searchfield_qt_private.cpp
|
||||
src/widgets/searchfield_qt_private.h
|
||||
Copyright: 2011, Mike McQuaid <mike@mikemcquaid.com>
|
||||
2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
License: Expat
|
||||
|
||||
Files: 3rdparty/SPMediaKeyTap/*
|
||||
Copyright: 2010, Spotify AB
|
||||
2011, Joachim Bengtsson
|
||||
License: BSD-3-clause
|
||||
|
||||
Files: 3rdparty/kdsingleapplication/*
|
||||
Copyright: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||
License: MIT
|
||||
|
||||
15
debian/rules
vendored
15
debian/rules
vendored
@@ -1,10 +1,17 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
export DH_VERBOSE=1
|
||||
export DEB_BUILD_MAINT_OPTIONS=hardening=+all
|
||||
%:
|
||||
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
|
||||
|
||||
override_dh_installchangelogs:
|
||||
dh_installchangelogs Changelog
|
||||
|
||||
%:
|
||||
dh $@
|
||||
override_dh_auto_test:
|
||||
|
||||
19
dist/CMakeLists.txt
vendored
19
dist/CMakeLists.txt
vendored
@@ -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_BINARY_DIR}/strawberry.spec @ONLY)
|
||||
endif()
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec.in ${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec @ONLY)
|
||||
endif(RPM_DISTRO AND RPM_DATE)
|
||||
|
||||
if(APPLE)
|
||||
if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET})
|
||||
@@ -9,13 +9,12 @@ if(APPLE)
|
||||
else()
|
||||
set(LSMinimumSystemVersion 12.0)
|
||||
endif()
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
||||
endif()
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist)
|
||||
endif(APPLE)
|
||||
|
||||
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()
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi.in ${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi @ONLY)
|
||||
endif(WIN32)
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
install(FILES ../data/icons/48x48/strawberry.png DESTINATION share/icons/hicolor/48x48/apps/)
|
||||
@@ -23,10 +22,10 @@ if(UNIX AND NOT APPLE)
|
||||
install(FILES ../data/icons/128x128/strawberry.png DESTINATION share/icons/hicolor/128x128/apps/)
|
||||
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()
|
||||
install(FILES unix/strawberry.1 unix/strawberry-tagreader.1 DESTINATION share/man/man1)
|
||||
endif(UNIX AND NOT APPLE)
|
||||
|
||||
if(APPLE)
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents")
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_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()
|
||||
|
||||
4
dist/macos/Info.plist.in
vendored
4
dist/macos/Info.plist.in
vendored
@@ -35,9 +35,9 @@
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>@LSMinimumSystemVersion@</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@</string>
|
||||
<string>https://www.strawberrymusicplayer.org/sparkle-macos</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=</string>
|
||||
<string>3IRScV8YtNVnx7zoeJAXvg28Kh1gN/Pyl2iPM467pG8=</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
29
dist/scripts/import-from-clementine.sh
vendored
29
dist/scripts/import-from-clementine.sh
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
# Strawberry Music Player
|
||||
# Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
# Copyright 2021, Alexey Vazhnov
|
||||
# 2021 Alexey Vazhnov
|
||||
#
|
||||
# Strawberry is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -19,7 +19,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
# Based on https://wiki.strawberrymusicplayer.org/wiki/Import_collection_library_and_playlists_from_Clementine
|
||||
# Based on https://github.com/strawberrymusicplayer/strawberry/wiki/Import-collection-library-and-playlists-data-from-Clementine
|
||||
|
||||
set -o nounset
|
||||
set -o errexit
|
||||
@@ -35,8 +35,8 @@ test -f "$FILE_DST" || { echo "No such file: $FILE_DST"; exit 1; }
|
||||
|
||||
echo "Will try to copy information from $FILE_SRC to $FILE_DST."
|
||||
echo
|
||||
echo 'This script will **delete all data** from the Strawberry database!'
|
||||
read -r -p 'Do you want to continue? (Only YES is accepted) ' answer
|
||||
echo 'This script will **delete all information** from Strawberry database!'
|
||||
read -r -p 'Do you want to continue? (the only YES is accepted) ' answer
|
||||
if [ "$answer" != "YES" ]; then exit 1; fi
|
||||
|
||||
# 'heredoc' with substitution of variables, see `man bash`, "Here Documents":
|
||||
@@ -62,9 +62,9 @@ INSERT INTO strawberry.subdirectories (directory_id, path, mtime) SELECT directo
|
||||
INSERT INTO strawberry.songs (ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory_id, url, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, compilation_detected, compilation_on, compilation_off, compilation_effective, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating)
|
||||
SELECT ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory, filename, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, sampler, forced_compilation_on, forced_compilation_off, effective_compilation, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating FROM clementine.songs WHERE unavailable = 0;
|
||||
UPDATE strawberry.songs SET source = 2;
|
||||
UPDATE strawberry.songs SET artist_id = '';
|
||||
UPDATE strawberry.songs SET album_id = '';
|
||||
UPDATE strawberry.songs SET song_id = '';
|
||||
UPDATE strawberry.songs SET artist_id = "";
|
||||
UPDATE strawberry.songs SET album_id = "";
|
||||
UPDATE strawberry.songs SET song_id = "";
|
||||
|
||||
/* Import playlists */
|
||||
|
||||
@@ -140,7 +140,7 @@ SELECT ROWID,
|
||||
bitrate,
|
||||
samplerate,
|
||||
directory,
|
||||
CASE WHEN filename IS NULL THEN '' ELSE filename END,
|
||||
filename,
|
||||
filetype,
|
||||
filesize,
|
||||
mtime,
|
||||
@@ -162,9 +162,16 @@ SELECT ROWID,
|
||||
|
||||
UPDATE strawberry.playlist_items SET source = 2;
|
||||
UPDATE strawberry.playlist_items SET type = 2;
|
||||
UPDATE strawberry.playlist_items SET artist_id = '';
|
||||
UPDATE strawberry.playlist_items SET album_id = '';
|
||||
UPDATE strawberry.playlist_items SET song_id = '';
|
||||
UPDATE strawberry.playlist_items SET artist_id = "";
|
||||
UPDATE strawberry.playlist_items SET album_id = "";
|
||||
UPDATE strawberry.playlist_items SET song_id = "";
|
||||
|
||||
/* Recreate the FTS tables */
|
||||
|
||||
DELETE FROM strawberry.songs_fts;
|
||||
INSERT INTO strawberry.songs_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)
|
||||
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment
|
||||
FROM strawberry.songs;
|
||||
|
||||
EOF
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<provides>
|
||||
<binary>strawberry</binary>
|
||||
<binary>strawberry-tagreader</binary>
|
||||
</provides>
|
||||
<name>Strawberry Music Player</name>
|
||||
<summary>A music player and collection organizer</summary>
|
||||
@@ -14,7 +15,7 @@
|
||||
<developer id="net.jkvinge.jonas">
|
||||
<name>Jonas Kvinge</name>
|
||||
</developer>
|
||||
<translation type="qt">strawberry</translation>
|
||||
<translation type="gettext">strawberry</translation>
|
||||
<content_rating type="oars-1.1" />
|
||||
<description>
|
||||
<p>
|
||||
@@ -51,13 +52,6 @@
|
||||
</screenshots>
|
||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||
<releases>
|
||||
<release version="1.2.7" date="2025-01-31"/>
|
||||
<release version="1.2.6" date="2025-01-17"/>
|
||||
<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"/>
|
||||
<release version="1.1.2" date="2024-09-12"/>
|
||||
<release version="1.1.1" date="2024-07-22"/>
|
||||
|
||||
10
dist/unix/strawberry-tagreader.1
vendored
Normal file
10
dist/unix/strawberry-tagreader.1
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.TH STRAWBERRY-TAGREADER "1"
|
||||
.SH NAME
|
||||
strawberry-tagreader \- internal tag reader for strawberry
|
||||
.SH SYNOPSIS
|
||||
.B strawberry-tagreader
|
||||
.SH DESCRIPTION
|
||||
This program is used internally by Strawberry to parse tags in music files without exposing the whole application to crashes caused by malformed files. It is not meant to be run on its own.
|
||||
.SH "AUTHORS"
|
||||
.PP
|
||||
Strawberry main developer is Jonas Kvinge <jonas@jkvinge.net>.
|
||||
34
dist/unix/strawberry.spec.in
vendored
34
dist/unix/strawberry.spec.in
vendored
@@ -21,6 +21,7 @@ BuildRequires: gcc-c++
|
||||
BuildRequires: hicolor-icon-theme
|
||||
BuildRequires: make
|
||||
BuildRequires: git
|
||||
BuildRequires: gettext
|
||||
BuildRequires: desktop-file-utils
|
||||
%if 0%{?suse_version}
|
||||
BuildRequires: update-desktop-files
|
||||
@@ -37,7 +38,9 @@ BuildRequires: pkgconfig(glib-2.0)
|
||||
BuildRequires: pkgconfig(gio-2.0)
|
||||
BuildRequires: pkgconfig(gio-unix-2.0)
|
||||
BuildRequires: pkgconfig(gthread-2.0)
|
||||
BuildRequires: pkgconfig(dbus-1)
|
||||
BuildRequires: pkgconfig(alsa)
|
||||
BuildRequires: pkgconfig(protobuf)
|
||||
BuildRequires: pkgconfig(sqlite3) >= 3.9
|
||||
BuildRequires: pkgconfig(taglib)
|
||||
BuildRequires: pkgconfig(fftw3)
|
||||
@@ -52,6 +55,9 @@ BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Gui)
|
||||
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Widgets)
|
||||
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Test)
|
||||
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@LinguistTools)
|
||||
%if "@QT_VERSION_MAJOR@" == "5"
|
||||
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@X11Extras)
|
||||
%endif
|
||||
BuildRequires: pkgconfig(gstreamer-1.0)
|
||||
BuildRequires: pkgconfig(gstreamer-app-1.0)
|
||||
BuildRequires: pkgconfig(gstreamer-audio-1.0)
|
||||
@@ -63,12 +69,18 @@ BuildRequires: pkgconfig(libcdio)
|
||||
BuildRequires: pkgconfig(libebur128)
|
||||
BuildRequires: pkgconfig(libgpod-1.0)
|
||||
BuildRequires: pkgconfig(libmtp)
|
||||
BuildRequires: cmake(GTest)
|
||||
BuildRequires: pkgconfig(gmock)
|
||||
%if 0%{?suse_version} || 0%{?fedora_version}
|
||||
BuildRequires: pkgconfig(libvlc)
|
||||
%endif
|
||||
|
||||
%if 0%{?suse_version}
|
||||
%if "@QT_VERSION_MAJOR@" == "6"
|
||||
Requires: qt6-sql-sqlite
|
||||
Requires: qt6-network-tls
|
||||
%endif
|
||||
%if "@QT_VERSION_MAJOR@" == "5"
|
||||
Requires: libQt5Sql5-sqlite
|
||||
%endif
|
||||
%endif
|
||||
|
||||
%description
|
||||
@@ -105,13 +117,13 @@ Features:
|
||||
|
||||
%build
|
||||
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
|
||||
export CXXFLAGS="-fPIC -Wno-maybe-uninitialized $RPM_OPT_FLAGS"
|
||||
export CXXFLAGS="-fPIC $RPM_OPT_FLAGS"
|
||||
%endif
|
||||
%if "%{?_vendor}" == "openmandriva"
|
||||
%{cmake} -DBUILD_WERROR=ON
|
||||
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@ -DENABLE_TRANSLATIONS=OFF
|
||||
%make_build
|
||||
%else
|
||||
%{cmake} -DBUILD_WERROR=ON
|
||||
%{cmake} -DQT_VERSION_MAJOR=@QT_VERSION_MAJOR@
|
||||
%cmake_build
|
||||
%endif
|
||||
|
||||
@@ -122,13 +134,11 @@ Features:
|
||||
%cmake_install
|
||||
%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
|
||||
%if 0%{?suse_version}
|
||||
%suse_update_desktop_file org.strawberrymusicplayer.strawberry Qt AudioVideo Audio Player
|
||||
%endif
|
||||
|
||||
%check
|
||||
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
|
||||
@@ -141,9 +151,11 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/org.strawberrymusicpl
|
||||
%doc README.md Changelog
|
||||
%license COPYING
|
||||
%{_bindir}/strawberry
|
||||
%{_bindir}/strawberry-tagreader
|
||||
%{_datadir}/applications/*.desktop
|
||||
%{_datadir}/icons/hicolor/*/apps/strawberry.*
|
||||
%{_mandir}/man1/%{name}.1.*
|
||||
%{_mandir}/man1/%{name}-tagreader.1.*
|
||||
%if 0%{?suse_version}
|
||||
%{_datadir}/metainfo/*.appdata.xml
|
||||
%else
|
||||
|
||||
140
dist/windows/strawberry.nsi.in
vendored
140
dist/windows/strawberry.nsi.in
vendored
@@ -208,16 +208,14 @@ FunctionEnd
|
||||
!ifdef msvc
|
||||
!define vc_redist_file "vc_redist.${arch}.exe"
|
||||
Function InstallMSVCRuntime
|
||||
SetOutPath "$TEMP"
|
||||
File "${vc_redist_file}"
|
||||
; ${registry::Read} "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${arch}" "Version" $R0 $R1
|
||||
; ${If} $R0 == ""
|
||||
${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
|
||||
|
||||
@@ -238,6 +236,7 @@ Section "Strawberry" Strawberry
|
||||
; Common executables
|
||||
|
||||
File "strawberry.exe"
|
||||
File "strawberry-tagreader.exe"
|
||||
File "strawberry.ico"
|
||||
File "sqlite3.exe"
|
||||
File "gst-launch-1.0.exe"
|
||||
@@ -274,7 +273,6 @@ Section "Strawberry" Strawberry
|
||||
File "libffi-8.dll"
|
||||
File "libfreetype-6.dll"
|
||||
File "libgcrypt-20.dll"
|
||||
File "libgetopt.dll"
|
||||
File "libgio-2.0-0.dll"
|
||||
File "libglib-2.0-0.dll"
|
||||
File "libgme.dll"
|
||||
@@ -331,6 +329,7 @@ Section "Strawberry" Strawberry
|
||||
File "libtasn1-6.dll"
|
||||
File "libtwolame-0.dll"
|
||||
File "libunistring-5.dll"
|
||||
File "libutf8_validity.dll"
|
||||
File "libvorbis-0.dll"
|
||||
File "libvorbisenc-2.dll"
|
||||
File "libvorbisfile-3.dll"
|
||||
@@ -339,6 +338,48 @@ Section "Strawberry" Strawberry
|
||||
File "libzstd.dll"
|
||||
File "zlib1.dll"
|
||||
|
||||
File "libabsl_base.dll"
|
||||
File "libabsl_city.dll"
|
||||
File "libabsl_cord.dll"
|
||||
File "libabsl_cord_internal.dll"
|
||||
File "libabsl_cordz_handle.dll"
|
||||
File "libabsl_cordz_info.dll"
|
||||
File "libabsl_crc32c.dll"
|
||||
File "libabsl_crc_cord_state.dll"
|
||||
File "libabsl_crc_internal.dll"
|
||||
File "libabsl_die_if_null.dll"
|
||||
File "libabsl_examine_stack.dll"
|
||||
File "libabsl_hash.dll"
|
||||
File "libabsl_int128.dll"
|
||||
File "libabsl_kernel_timeout_internal.dll"
|
||||
File "libabsl_log_globals.dll"
|
||||
File "libabsl_log_internal_check_op.dll"
|
||||
File "libabsl_log_internal_conditions.dll"
|
||||
File "libabsl_log_internal_format.dll"
|
||||
File "libabsl_log_internal_globals.dll"
|
||||
File "libabsl_log_internal_log_sink_set.dll"
|
||||
File "libabsl_log_internal_message.dll"
|
||||
File "libabsl_log_internal_nullguard.dll"
|
||||
File "libabsl_log_internal_proto.dll"
|
||||
File "libabsl_log_sink.dll"
|
||||
File "libabsl_low_level_hash.dll"
|
||||
File "libabsl_malloc_internal.dll"
|
||||
File "libabsl_raw_hash_set.dll"
|
||||
File "libabsl_raw_logging_internal.dll"
|
||||
File "libabsl_spinlock_wait.dll"
|
||||
File "libabsl_stacktrace.dll"
|
||||
File "libabsl_status.dll"
|
||||
File "libabsl_statusor.dll"
|
||||
File "libabsl_strerror.dll"
|
||||
File "libabsl_str_format_internal.dll"
|
||||
File "libabsl_strings.dll"
|
||||
File "libabsl_strings_internal.dll"
|
||||
File "libabsl_symbolize.dll"
|
||||
File "libabsl_synchronization.dll"
|
||||
File "libabsl_throw_delegate.dll"
|
||||
File "libabsl_time.dll"
|
||||
File "libabsl_time_zone.dll"
|
||||
|
||||
!ifdef debug
|
||||
File "gdb.exe"
|
||||
File "libexpat-1.dll"
|
||||
@@ -348,6 +389,7 @@ Section "Strawberry" Strawberry
|
||||
File "libpcre2-16d.dll"
|
||||
File "libreadline8.dll"
|
||||
File "libtermcap.dll"
|
||||
File "libabsl_graphcycles_internal.dll"
|
||||
!else
|
||||
File "libpcre2-8.dll"
|
||||
File "libpcre2-16.dll"
|
||||
@@ -376,12 +418,10 @@ Section "Strawberry" Strawberry
|
||||
File "faad-2.dll"
|
||||
File "fdk-aac.dll"
|
||||
File "ffi-7.dll"
|
||||
File "getopt.dll"
|
||||
File "gio-2.0-0.dll"
|
||||
File "glib-2.0-0.dll"
|
||||
File "gme.dll"
|
||||
File "gmodule-2.0-0.dll"
|
||||
File "gnutls.dll"
|
||||
File "gobject-2.0-0.dll"
|
||||
File "gstadaptivedemux-1.0-0.dll"
|
||||
File "gstapp-1.0-0.dll"
|
||||
@@ -412,6 +452,7 @@ Section "Strawberry" Strawberry
|
||||
File "liblzma.dll"
|
||||
File "libmp3lame.dll"
|
||||
File "libopenmpt.dll"
|
||||
File "utf8_validity.dll"
|
||||
File "mpcdec.dll"
|
||||
File "mpg123.dll"
|
||||
File "nghttp2.dll"
|
||||
@@ -426,6 +467,7 @@ Section "Strawberry" Strawberry
|
||||
File "vorbis.dll"
|
||||
File "vorbisfile.dll"
|
||||
File "wavpackdll.dll"
|
||||
File "abseil_dll.dll"
|
||||
|
||||
!ifdef release
|
||||
File "freetype.dll"
|
||||
@@ -458,11 +500,16 @@ Section "Strawberry" Strawberry
|
||||
|
||||
; Common files
|
||||
|
||||
File "icudt76.dll"
|
||||
File "icudt75.dll"
|
||||
File "libfftw3-3.dll"
|
||||
!ifdef debug
|
||||
File "libprotobufd.dll"
|
||||
!else
|
||||
File "libprotobuf.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
File "icuin76d.dll"
|
||||
File "icuuc76d.dll"
|
||||
File "icuin75d.dll"
|
||||
File "icuuc75d.dll"
|
||||
File "libxml2d.dll"
|
||||
File "Qt6Concurrentd.dll"
|
||||
File "Qt6Cored.dll"
|
||||
@@ -471,8 +518,8 @@ Section "Strawberry" Strawberry
|
||||
File "Qt6Sqld.dll"
|
||||
File "Qt6Widgetsd.dll"
|
||||
!else
|
||||
File "icuin76.dll"
|
||||
File "icuuc76.dll"
|
||||
File "icuin75.dll"
|
||||
File "icuuc75.dll"
|
||||
File "libxml2.dll"
|
||||
File "Qt6Concurrent.dll"
|
||||
File "Qt6Core.dll"
|
||||
@@ -761,6 +808,7 @@ Section "Uninstall"
|
||||
; Delete all the files
|
||||
|
||||
Delete "$INSTDIR\strawberry.exe"
|
||||
Delete "$INSTDIR\strawberry-tagreader.exe"
|
||||
Delete "$INSTDIR\strawberry.ico"
|
||||
Delete "$INSTDIR\sqlite3.exe"
|
||||
Delete "$INSTDIR\gst-launch-1.0.exe"
|
||||
@@ -798,7 +846,6 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libffi-8.dll"
|
||||
Delete "$INSTDIR\libfreetype-6.dll"
|
||||
Delete "$INSTDIR\libgcrypt-20.dll"
|
||||
Delete "$INSTDIR\libgetopt.dll"
|
||||
Delete "$INSTDIR\libgio-2.0-0.dll"
|
||||
Delete "$INSTDIR\libglib-2.0-0.dll"
|
||||
Delete "$INSTDIR\libgme.dll"
|
||||
@@ -855,6 +902,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libtasn1-6.dll"
|
||||
Delete "$INSTDIR\libtwolame-0.dll"
|
||||
Delete "$INSTDIR\libunistring-5.dll"
|
||||
Delete "$INSTDIR\libutf8_validity.dll"
|
||||
Delete "$INSTDIR\libvorbis-0.dll"
|
||||
Delete "$INSTDIR\libvorbisenc-2.dll"
|
||||
Delete "$INSTDIR\libvorbisfile-3.dll"
|
||||
@@ -863,6 +911,48 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libzstd.dll"
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
|
||||
Delete "$INSTDIR\libabsl_base.dll"
|
||||
Delete "$INSTDIR\libabsl_city.dll"
|
||||
Delete "$INSTDIR\libabsl_cord.dll"
|
||||
Delete "$INSTDIR\libabsl_cord_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_cordz_handle.dll"
|
||||
Delete "$INSTDIR\libabsl_cordz_info.dll"
|
||||
Delete "$INSTDIR\libabsl_crc32c.dll"
|
||||
Delete "$INSTDIR\libabsl_crc_cord_state.dll"
|
||||
Delete "$INSTDIR\libabsl_crc_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_die_if_null.dll"
|
||||
Delete "$INSTDIR\libabsl_examine_stack.dll"
|
||||
Delete "$INSTDIR\libabsl_hash.dll"
|
||||
Delete "$INSTDIR\libabsl_int128.dll"
|
||||
Delete "$INSTDIR\libabsl_kernel_timeout_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_log_globals.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_check_op.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_conditions.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_format.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_globals.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_log_sink_set.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_message.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_nullguard.dll"
|
||||
Delete "$INSTDIR\libabsl_log_internal_proto.dll"
|
||||
Delete "$INSTDIR\libabsl_log_sink.dll"
|
||||
Delete "$INSTDIR\libabsl_low_level_hash.dll"
|
||||
Delete "$INSTDIR\libabsl_malloc_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_raw_hash_set.dll"
|
||||
Delete "$INSTDIR\libabsl_raw_logging_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_spinlock_wait.dll"
|
||||
Delete "$INSTDIR\libabsl_stacktrace.dll"
|
||||
Delete "$INSTDIR\libabsl_status.dll"
|
||||
Delete "$INSTDIR\libabsl_statusor.dll"
|
||||
Delete "$INSTDIR\libabsl_strerror.dll"
|
||||
Delete "$INSTDIR\libabsl_str_format_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_strings.dll"
|
||||
Delete "$INSTDIR\libabsl_strings_internal.dll"
|
||||
Delete "$INSTDIR\libabsl_symbolize.dll"
|
||||
Delete "$INSTDIR\libabsl_synchronization.dll"
|
||||
Delete "$INSTDIR\libabsl_throw_delegate.dll"
|
||||
Delete "$INSTDIR\libabsl_time.dll"
|
||||
Delete "$INSTDIR\libabsl_time_zone.dll"
|
||||
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\gdb.exe"
|
||||
Delete "$INSTDIR\libexpat-1.dll"
|
||||
@@ -872,6 +962,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libpcre2-16d.dll"
|
||||
Delete "$INSTDIR\libreadline8.dll"
|
||||
Delete "$INSTDIR\libtermcap.dll"
|
||||
Delete "$INSTDIR\libabsl_graphcycles_internal.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libpcre2-8.dll"
|
||||
Delete "$INSTDIR\libpcre2-16.dll"
|
||||
@@ -900,12 +991,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\faad-2.dll"
|
||||
Delete "$INSTDIR\fdk-aac.dll"
|
||||
Delete "$INSTDIR\ffi-7.dll"
|
||||
Delete "$INSTDIR\getopt.dll"
|
||||
Delete "$INSTDIR\gio-2.0-0.dll"
|
||||
Delete "$INSTDIR\glib-2.0-0.dll"
|
||||
Delete "$INSTDIR\gme.dll"
|
||||
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
Delete "$INSTDIR\gobject-2.0-0.dll"
|
||||
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstapp-1.0-0.dll"
|
||||
@@ -936,6 +1025,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\liblzma.dll"
|
||||
Delete "$INSTDIR\libmp3lame.dll"
|
||||
Delete "$INSTDIR\libopenmpt.dll"
|
||||
Delete "$INSTDIR\utf8_validity.dll"
|
||||
Delete "$INSTDIR\mpcdec.dll"
|
||||
Delete "$INSTDIR\mpg123.dll"
|
||||
Delete "$INSTDIR\nghttp2.dll"
|
||||
@@ -950,6 +1040,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\vorbis.dll"
|
||||
Delete "$INSTDIR\vorbisfile.dll"
|
||||
Delete "$INSTDIR\wavpackdll.dll"
|
||||
Delete "$INSTDIR\abseil_dll.dll"
|
||||
|
||||
!ifdef release
|
||||
Delete "$INSTDIR\freetype.dll"
|
||||
@@ -981,11 +1072,16 @@ Section "Uninstall"
|
||||
|
||||
; Common files
|
||||
|
||||
Delete "$INSTDIR\icudt76.dll"
|
||||
Delete "$INSTDIR\icudt75.dll"
|
||||
Delete "$INSTDIR\libfftw3-3.dll"
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\libprotobufd.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libprotobuf.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
Delete "$INSTDIR\icuin76d.dll"
|
||||
Delete "$INSTDIR\icuuc76d.dll"
|
||||
Delete "$INSTDIR\icuin75d.dll"
|
||||
Delete "$INSTDIR\icuuc75d.dll"
|
||||
Delete "$INSTDIR\libxml2d.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrentd.dll"
|
||||
Delete "$INSTDIR\Qt6Cored.dll"
|
||||
@@ -994,8 +1090,8 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\Qt6Sqld.dll"
|
||||
Delete "$INSTDIR\Qt6Widgetsd.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\icuin76.dll"
|
||||
Delete "$INSTDIR\icuuc76.dll"
|
||||
Delete "$INSTDIR\icuin75.dll"
|
||||
Delete "$INSTDIR\icuuc75.dll"
|
||||
Delete "$INSTDIR\libxml2.dll"
|
||||
Delete "$INSTDIR\Qt6Concurrent.dll"
|
||||
Delete "$INSTDIR\Qt6Core.dll"
|
||||
|
||||
2
dist/windows/windres.rc.in
vendored
2
dist/windows/windres.rc.in
vendored
@@ -1,4 +1,4 @@
|
||||
strawberry ICON "${CMAKE_SOURCE_DIR}/dist/windows/strawberry.ico"
|
||||
strawberry ICON "${CMAKE_CURRENT_SOURCE_DIR}/../dist/windows/strawberry.ico"
|
||||
1 VERSIONINFO
|
||||
FILEVERSION ${STRAWBERRY_VERSION_MAJOR},${STRAWBERRY_VERSION_MINOR},${STRAWBERRY_VERSION_PATCH}
|
||||
PRODUCTVERSION ${STRAWBERRY_VERSION_MAJOR},${STRAWBERRY_VERSION_MINOR},${STRAWBERRY_VERSION_PATCH}
|
||||
|
||||
35
ext/gstmoodbar/CMakeLists.txt
Normal file
35
ext/gstmoodbar/CMakeLists.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(SOURCES gstfastspectrum.cpp gstmoodbarplugin.cpp)
|
||||
|
||||
add_library(gstmoodbar STATIC ${SOURCES})
|
||||
|
||||
target_include_directories(gstmoodbar SYSTEM PRIVATE
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
${GOBJECT_INCLUDE_DIRS}
|
||||
${GSTREAMER_INCLUDE_DIRS}
|
||||
${GSTREAMER_BASE_INCLUDE_DIRS}
|
||||
${GSTREAMER_AUDIO_INCLUDE_DIRS}
|
||||
${FFTW3_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_include_directories(gstmoodbar PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_directories(gstmoodbar PRIVATE
|
||||
${GLIB_LIBRARY_DIRS}
|
||||
${GOBJECT_LIBRARY_DIRS}
|
||||
${GSTREAMER_LIBRARY_DIRS}
|
||||
${GSTREAMER_BASE_LIBRARY_DIRS}
|
||||
${GSTREAMER_AUDIO_LIBRARY_DIRS}
|
||||
${FFTW3_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
target_link_libraries(gstmoodbar PRIVATE
|
||||
${GLIB_LIBRARIES}
|
||||
${GOBJECT_LIBRARIES}
|
||||
${GSTREAMER_LIBRARIES}
|
||||
${GSTREAMER_BASE_LIBRARIES}
|
||||
${GSTREAMER_AUDIO_LIBRARIES}
|
||||
${FFTW3_FFTW_LIBRARY}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
)
|
||||
520
ext/gstmoodbar/gstfastspectrum.cpp
Normal file
520
ext/gstmoodbar/gstfastspectrum.cpp
Normal file
@@ -0,0 +1,520 @@
|
||||
/* GStreamer
|
||||
* Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
|
||||
* <2006,2011> Stefan Kost <ensonic@users.sf.net>
|
||||
* <2007-2009> Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/audio/gstaudiofilter.h>
|
||||
|
||||
#include <QMutex>
|
||||
|
||||
#include "gstfastspectrum.h"
|
||||
|
||||
GST_DEBUG_CATEGORY_STATIC(gst_fastspectrum_debug);
|
||||
|
||||
namespace {
|
||||
|
||||
// Spectrum properties
|
||||
constexpr auto DEFAULT_INTERVAL = (GST_SECOND / 10);
|
||||
constexpr auto DEFAULT_BANDS = 128;
|
||||
|
||||
enum {
|
||||
PROP_0,
|
||||
PROP_INTERVAL,
|
||||
PROP_BANDS
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#define gst_fastspectrum_parent_class parent_class
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wold-style-cast"
|
||||
#endif
|
||||
G_DEFINE_TYPE(GstFastSpectrum, gst_fastspectrum, GST_TYPE_AUDIO_FILTER)
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
static void gst_fastspectrum_finalize(GObject *object);
|
||||
static void gst_fastspectrum_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
|
||||
static void gst_fastspectrum_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
|
||||
static gboolean gst_fastspectrum_start(GstBaseTransform *trans);
|
||||
static gboolean gst_fastspectrum_stop(GstBaseTransform *trans);
|
||||
static GstFlowReturn gst_fastspectrum_transform_ip(GstBaseTransform *trans, GstBuffer *buffer);
|
||||
static gboolean gst_fastspectrum_setup(GstAudioFilter *base, const GstAudioInfo *info);
|
||||
|
||||
static void gst_fastspectrum_class_init(GstFastSpectrumClass *klass) {
|
||||
|
||||
GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
|
||||
GstElementClass *element_class = GST_ELEMENT_CLASS(klass);
|
||||
GstBaseTransformClass *trans_class = GST_BASE_TRANSFORM_CLASS(klass);
|
||||
GstAudioFilterClass *filter_class = GST_AUDIO_FILTER_CLASS(klass);
|
||||
GstCaps *caps = nullptr;
|
||||
|
||||
gobject_class->set_property = gst_fastspectrum_set_property;
|
||||
gobject_class->get_property = gst_fastspectrum_get_property;
|
||||
gobject_class->finalize = gst_fastspectrum_finalize;
|
||||
|
||||
trans_class->start = GST_DEBUG_FUNCPTR(gst_fastspectrum_start);
|
||||
trans_class->stop = GST_DEBUG_FUNCPTR(gst_fastspectrum_stop);
|
||||
trans_class->transform_ip = GST_DEBUG_FUNCPTR(gst_fastspectrum_transform_ip);
|
||||
trans_class->passthrough_on_same_caps = TRUE;
|
||||
|
||||
filter_class->setup = GST_DEBUG_FUNCPTR(gst_fastspectrum_setup);
|
||||
|
||||
g_object_class_install_property(gobject_class, PROP_INTERVAL, g_param_spec_uint64("interval", "Interval", "Interval of time between message posts (in nanoseconds)", 1, G_MAXUINT64, DEFAULT_INTERVAL, static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
||||
|
||||
g_object_class_install_property(gobject_class, PROP_BANDS, g_param_spec_uint("bands", "Bands", "Number of frequency bands", 0, G_MAXUINT, DEFAULT_BANDS, static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
|
||||
|
||||
GST_DEBUG_CATEGORY_INIT(gst_fastspectrum_debug, "spectrum", 0, "audio spectrum analyser element");
|
||||
|
||||
gst_element_class_set_static_metadata(element_class, "Spectrum analyzer",
|
||||
"Filter/Analyzer/Audio",
|
||||
"Run an FFT on the audio signal, output spectrum data",
|
||||
"Erik Walthinsen <omega@cse.ogi.edu>, "
|
||||
"Stefan Kost <ensonic@users.sf.net>, "
|
||||
"Sebastian Dröge <sebastian.droege@collabora.co.uk>");
|
||||
|
||||
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
||||
caps = gst_caps_from_string(GST_AUDIO_CAPS_MAKE("{ S16LE, S24LE, S32LE, F32LE, F64LE }") ", layout = (string) interleaved, channels = 1");
|
||||
#else
|
||||
caps = gst_caps_from_string(GST_AUDIO_CAPS_MAKE("{ S16BE, S24BE, S32BE, F32BE, F64BE }") ", layout = (string) interleaved, channels = 1");
|
||||
#endif
|
||||
|
||||
gst_audio_filter_class_add_pad_templates(filter_class, caps);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
klass->fftw_lock = new QMutex;
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_init(GstFastSpectrum *spectrum) {
|
||||
|
||||
spectrum->interval = DEFAULT_INTERVAL;
|
||||
spectrum->bands = DEFAULT_BANDS;
|
||||
|
||||
spectrum->channel_data_initialized = false;
|
||||
|
||||
g_mutex_init(&spectrum->lock);
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_alloc_channel_data(GstFastSpectrum *spectrum) {
|
||||
|
||||
guint bands = spectrum->bands;
|
||||
guint nfft = 2 * bands - 2;
|
||||
|
||||
spectrum->input_ring_buffer = new double[nfft];
|
||||
spectrum->fft_input = reinterpret_cast<double*>(fftw_malloc(sizeof(double) * nfft));
|
||||
spectrum->fft_output = reinterpret_cast<fftw_complex*>(fftw_malloc(sizeof(fftw_complex) * (nfft / 2 + 1)));
|
||||
|
||||
spectrum->spect_magnitude = new double[bands] {};
|
||||
|
||||
GstFastSpectrumClass *klass = reinterpret_cast<GstFastSpectrumClass*>(G_OBJECT_GET_CLASS(spectrum));
|
||||
{
|
||||
QMutexLocker l(klass->fftw_lock);
|
||||
spectrum->plan = fftw_plan_dft_r2c_1d(static_cast<int>(nfft), spectrum->fft_input, spectrum->fft_output, FFTW_ESTIMATE);
|
||||
}
|
||||
spectrum->channel_data_initialized = true;
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_free_channel_data(GstFastSpectrum *spectrum) {
|
||||
|
||||
GstFastSpectrumClass *klass = reinterpret_cast<GstFastSpectrumClass*>(G_OBJECT_GET_CLASS(spectrum));
|
||||
if (spectrum->channel_data_initialized) {
|
||||
{
|
||||
QMutexLocker l(klass->fftw_lock);
|
||||
fftw_destroy_plan(spectrum->plan);
|
||||
}
|
||||
fftw_free(spectrum->fft_input);
|
||||
fftw_free(spectrum->fft_output);
|
||||
delete[] spectrum->input_ring_buffer;
|
||||
delete[] spectrum->spect_magnitude;
|
||||
|
||||
spectrum->channel_data_initialized = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_flush(GstFastSpectrum *spectrum) {
|
||||
|
||||
spectrum->num_frames = 0;
|
||||
spectrum->num_fft = 0;
|
||||
|
||||
spectrum->accumulated_error = 0;
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_reset_state(GstFastSpectrum *spectrum) {
|
||||
|
||||
GST_DEBUG_OBJECT(spectrum, "resetting state");
|
||||
|
||||
gst_fastspectrum_free_channel_data(spectrum);
|
||||
gst_fastspectrum_flush(spectrum);
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_finalize(GObject *object) {
|
||||
|
||||
GstFastSpectrum *spectrum = reinterpret_cast<GstFastSpectrum*>(object);
|
||||
|
||||
gst_fastspectrum_reset_state(spectrum);
|
||||
g_mutex_clear(&spectrum->lock);
|
||||
|
||||
G_OBJECT_CLASS(parent_class)->finalize(object);
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) {
|
||||
|
||||
GstFastSpectrum *filter = reinterpret_cast<GstFastSpectrum*>(object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_INTERVAL: {
|
||||
guint64 interval = g_value_get_uint64(value);
|
||||
g_mutex_lock(&filter->lock);
|
||||
if (filter->interval != interval) {
|
||||
filter->interval = interval;
|
||||
gst_fastspectrum_reset_state(filter);
|
||||
}
|
||||
g_mutex_unlock(&filter->lock);
|
||||
break;
|
||||
}
|
||||
case PROP_BANDS: {
|
||||
guint bands = g_value_get_uint(value);
|
||||
g_mutex_lock(&filter->lock);
|
||||
if (filter->bands != bands) {
|
||||
filter->bands = bands;
|
||||
gst_fastspectrum_reset_state(filter);
|
||||
}
|
||||
g_mutex_unlock(&filter->lock);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) {
|
||||
|
||||
GstFastSpectrum *filter = reinterpret_cast<GstFastSpectrum*>(object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_INTERVAL:
|
||||
g_value_set_uint64(value, filter->interval);
|
||||
break;
|
||||
case PROP_BANDS:
|
||||
g_value_set_uint(value, filter->bands);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static gboolean gst_fastspectrum_start(GstBaseTransform *trans) {
|
||||
|
||||
GstFastSpectrum *spectrum = reinterpret_cast<GstFastSpectrum*>(trans);
|
||||
|
||||
gst_fastspectrum_reset_state(spectrum);
|
||||
|
||||
return TRUE;
|
||||
|
||||
}
|
||||
|
||||
static gboolean gst_fastspectrum_stop(GstBaseTransform *trans) {
|
||||
|
||||
GstFastSpectrum *spectrum = reinterpret_cast<GstFastSpectrum*>(trans);
|
||||
|
||||
gst_fastspectrum_reset_state(spectrum);
|
||||
|
||||
return TRUE;
|
||||
|
||||
}
|
||||
|
||||
// Mixing data readers
|
||||
|
||||
static void input_data_mixed_float(const guint8 *_in, double *out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
Q_UNUSED(max_value);
|
||||
|
||||
const gfloat *in = reinterpret_cast<const gfloat*>(_in);
|
||||
guint ip = 0;
|
||||
|
||||
for (guint j = 0; j < len; j++) {
|
||||
out[op] = in[ip++];
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_double(const guint8 *_in, double *out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
Q_UNUSED(max_value);
|
||||
|
||||
const gdouble *in = reinterpret_cast<const gdouble*>(_in);
|
||||
guint ip = 0;
|
||||
|
||||
for (guint j = 0; j < len; j++) {
|
||||
out[op] = in[ip++];
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_int32_max(const guint8 *_in, double *out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
const gint32 *in = reinterpret_cast<const gint32*>(_in);
|
||||
guint ip = 0;
|
||||
|
||||
for (guint j = 0; j < len; j++) {
|
||||
out[op] = in[ip++] / max_value;
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_int24_max(const guint8 *_in, double *out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
for (guint j = 0; j < len; j++) {
|
||||
#if G_BYTE_ORDER == G_BIG_ENDIAN
|
||||
guint32 value = GST_READ_UINT24_BE(_in);
|
||||
#else
|
||||
guint32 value = GST_READ_UINT24_LE(_in);
|
||||
#endif
|
||||
if (value & 0x00800000) {
|
||||
value |= 0xff000000;
|
||||
}
|
||||
|
||||
out[op] = value / max_value;
|
||||
op = (op + 1) % nfft;
|
||||
_in += 3;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void input_data_mixed_int16_max(const guint8 *_in, double *out, guint len, double max_value, guint op, guint nfft) {
|
||||
|
||||
const gint16 *in = reinterpret_cast<const gint16*>(_in);
|
||||
guint ip = 0;
|
||||
|
||||
for (guint j = 0; j < len; j++) {
|
||||
out[op] = in[ip++] / max_value;
|
||||
op = (op + 1) % nfft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static gboolean gst_fastspectrum_setup(GstAudioFilter *base, const GstAudioInfo *info) {
|
||||
|
||||
GstFastSpectrum *spectrum = reinterpret_cast<GstFastSpectrum*>(base);
|
||||
GstFastSpectrumInputData input_data = nullptr;
|
||||
|
||||
g_mutex_lock(&spectrum->lock);
|
||||
switch (GST_AUDIO_INFO_FORMAT(info)) {
|
||||
case GST_AUDIO_FORMAT_S16:
|
||||
input_data = input_data_mixed_int16_max;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_S24:
|
||||
input_data = input_data_mixed_int24_max;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_S32:
|
||||
input_data = input_data_mixed_int32_max;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_F32:
|
||||
input_data = input_data_mixed_float;
|
||||
break;
|
||||
case GST_AUDIO_FORMAT_F64:
|
||||
input_data = input_data_mixed_double;
|
||||
break;
|
||||
default:
|
||||
g_assert_not_reached();
|
||||
break;
|
||||
}
|
||||
spectrum->input_data = input_data;
|
||||
|
||||
gst_fastspectrum_reset_state(spectrum);
|
||||
g_mutex_unlock(&spectrum->lock);
|
||||
|
||||
return TRUE;
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_run_fft(GstFastSpectrum *spectrum, guint input_pos) {
|
||||
|
||||
guint bands = spectrum->bands;
|
||||
guint nfft = 2 * bands - 2;
|
||||
|
||||
for (guint i = 0; i < nfft; i++) {
|
||||
spectrum->fft_input[i] = spectrum->input_ring_buffer[(input_pos + i) % nfft];
|
||||
}
|
||||
|
||||
// Should be safe to execute the same plan multiple times in parallel.
|
||||
fftw_execute(spectrum->plan);
|
||||
|
||||
// Calculate magnitude in db
|
||||
for (guint i = 0; i < bands; i++) {
|
||||
gdouble val = spectrum->fft_output[i][0] * spectrum->fft_output[i][0];
|
||||
val += spectrum->fft_output[i][1] * spectrum->fft_output[i][1];
|
||||
val /= nfft * nfft;
|
||||
spectrum->spect_magnitude[i] += val;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static GstFlowReturn gst_fastspectrum_transform_ip(GstBaseTransform *trans, GstBuffer *buffer) {
|
||||
|
||||
GstFastSpectrum *spectrum = reinterpret_cast<GstFastSpectrum*>(trans);
|
||||
guint rate = GST_AUDIO_FILTER_RATE(spectrum);
|
||||
guint bps = GST_AUDIO_FILTER_BPS(spectrum);
|
||||
guint bpf = GST_AUDIO_FILTER_BPF(spectrum);
|
||||
double max_value = static_cast<double>((1UL << ((bps << 3) - 1)) - 1);
|
||||
guint bands = spectrum->bands;
|
||||
guint nfft = 2 * bands - 2;
|
||||
guint input_pos = 0;
|
||||
GstMapInfo map;
|
||||
const guint8 *data = nullptr;
|
||||
gsize size = 0;
|
||||
GstFastSpectrumInputData input_data = nullptr;
|
||||
|
||||
g_mutex_lock(&spectrum->lock);
|
||||
gst_buffer_map(buffer, &map, GST_MAP_READ);
|
||||
data = map.data;
|
||||
size = map.size;
|
||||
|
||||
GST_LOG_OBJECT(spectrum, "input size: %" G_GSIZE_FORMAT " bytes", size);
|
||||
|
||||
if (GST_BUFFER_IS_DISCONT(buffer)) {
|
||||
GST_DEBUG_OBJECT(spectrum, "Discontinuity detected -- flushing");
|
||||
gst_fastspectrum_flush(spectrum);
|
||||
}
|
||||
|
||||
// If we don't have a FFT context yet (or it was reset due to parameter changes) get one and allocate memory for everything
|
||||
if (!spectrum->channel_data_initialized) {
|
||||
GST_DEBUG_OBJECT(spectrum, "allocating for bands %u", bands);
|
||||
|
||||
gst_fastspectrum_alloc_channel_data(spectrum);
|
||||
|
||||
// Number of sample frames we process before posting a message interval is in ns
|
||||
spectrum->frames_per_interval = gst_util_uint64_scale(spectrum->interval, rate, GST_SECOND);
|
||||
spectrum->frames_todo = spectrum->frames_per_interval;
|
||||
// Rounding error for frames_per_interval in ns, aggregated it in accumulated_error
|
||||
spectrum->error_per_interval = (spectrum->interval * rate) % GST_SECOND;
|
||||
if (spectrum->frames_per_interval == 0) {
|
||||
spectrum->frames_per_interval = 1;
|
||||
}
|
||||
|
||||
GST_INFO_OBJECT(spectrum, "interval %" GST_TIME_FORMAT ", fpi %" G_GUINT64_FORMAT ", error %" GST_TIME_FORMAT, GST_TIME_ARGS(spectrum->interval), spectrum->frames_per_interval, GST_TIME_ARGS(spectrum->error_per_interval));
|
||||
|
||||
spectrum->input_pos = 0;
|
||||
|
||||
gst_fastspectrum_flush(spectrum);
|
||||
}
|
||||
|
||||
if (spectrum->num_frames == 0) {
|
||||
spectrum->message_ts = GST_BUFFER_TIMESTAMP(buffer);
|
||||
}
|
||||
|
||||
input_pos = spectrum->input_pos;
|
||||
input_data = spectrum->input_data;
|
||||
|
||||
while (size >= bpf) {
|
||||
// Run input_data for a chunk of data
|
||||
guint fft_todo = nfft - (spectrum->num_frames % nfft);
|
||||
guint msg_todo = spectrum->frames_todo - spectrum->num_frames;
|
||||
GST_LOG_OBJECT(spectrum, "message frames todo: %u, fft frames todo: %u, input frames %" G_GSIZE_FORMAT, msg_todo, fft_todo, (size / bpf));
|
||||
guint block_size = msg_todo;
|
||||
if (block_size > (size / bpf)) {
|
||||
block_size = (size / bpf);
|
||||
}
|
||||
if (block_size > fft_todo) {
|
||||
block_size = fft_todo;
|
||||
}
|
||||
|
||||
// Move the current frames into our ringbuffers
|
||||
input_data(data, spectrum->input_ring_buffer, block_size, max_value, input_pos, nfft);
|
||||
|
||||
data += block_size * bpf;
|
||||
size -= block_size * bpf;
|
||||
input_pos = (input_pos + block_size) % nfft;
|
||||
spectrum->num_frames += block_size;
|
||||
|
||||
gboolean have_full_interval = (spectrum->num_frames == spectrum->frames_todo);
|
||||
|
||||
GST_LOG_OBJECT(spectrum, "size: %" G_GSIZE_FORMAT ", do-fft = %d, do-message = %d", size, (spectrum->num_frames % nfft == 0), have_full_interval);
|
||||
|
||||
// If we have enough frames for an FFT or we have all frames required for the interval and we haven't run a FFT, then run an FFT
|
||||
if ((spectrum->num_frames % nfft == 0) || (have_full_interval && !spectrum->num_fft)) {
|
||||
gst_fastspectrum_run_fft(spectrum, input_pos);
|
||||
spectrum->num_fft++;
|
||||
}
|
||||
|
||||
// Do we have the FFTs for one interval?
|
||||
if (have_full_interval) {
|
||||
GST_DEBUG_OBJECT(spectrum, "nfft: %u frames: %" G_GUINT64_FORMAT " fpi: %" G_GUINT64_FORMAT " error: %" GST_TIME_FORMAT, nfft, spectrum->num_frames, spectrum->frames_per_interval, GST_TIME_ARGS(spectrum->accumulated_error));
|
||||
|
||||
spectrum->frames_todo = spectrum->frames_per_interval;
|
||||
if (spectrum->accumulated_error >= GST_SECOND) {
|
||||
spectrum->accumulated_error -= GST_SECOND;
|
||||
spectrum->frames_todo++;
|
||||
}
|
||||
spectrum->accumulated_error += spectrum->error_per_interval;
|
||||
|
||||
if (spectrum->output_callback) {
|
||||
// Calculate average
|
||||
for (guint i = 0; i < spectrum->bands; i++) {
|
||||
spectrum->spect_magnitude[i] /= static_cast<double>(spectrum->num_fft);
|
||||
}
|
||||
|
||||
spectrum->output_callback(spectrum->spect_magnitude, static_cast<int>(spectrum->bands));
|
||||
|
||||
// Reset spectrum accumulators
|
||||
memset(spectrum->spect_magnitude, 0, spectrum->bands * sizeof(double));
|
||||
}
|
||||
|
||||
if (GST_CLOCK_TIME_IS_VALID(spectrum->message_ts)) {
|
||||
spectrum->message_ts += gst_util_uint64_scale(spectrum->num_frames, GST_SECOND, rate);
|
||||
}
|
||||
|
||||
spectrum->num_frames = 0;
|
||||
spectrum->num_fft = 0;
|
||||
}
|
||||
}
|
||||
|
||||
spectrum->input_pos = input_pos;
|
||||
|
||||
gst_buffer_unmap(buffer, &map);
|
||||
g_mutex_unlock(&spectrum->lock);
|
||||
|
||||
g_assert(size == 0);
|
||||
|
||||
return GST_FLOW_OK;
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/* GStreamer
|
||||
* Copyright (C) <1999> Erik Walthinsen <omega@cse.ogi.edu>
|
||||
* Copyright (C) <2009> Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||
* Copyright (C) <2018-2024> Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
@@ -26,8 +25,9 @@
|
||||
// - Send output via a callback instead of GST messages (less overhead).
|
||||
// - Removed all properties except interval and band.
|
||||
|
||||
#ifndef GST_STRAWBERRY_FASTSPECTRUM_H
|
||||
#define GST_STRAWBERRY_FASTSPECTRUM_H
|
||||
|
||||
#ifndef GST_MOODBAR_FASTSPECTRUM_H
|
||||
#define GST_MOODBAR_FASTSPECTRUM_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
@@ -37,17 +37,19 @@
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define GST_TYPE_STRAWBERRY_FASTSPECTRUM (gst_strawberry_fastspectrum_get_type())
|
||||
#define GST_STRAWBERRY_FASTSPECTRUM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GST_TYPE_FASTSPECTRUM, GstStrawberryFastSpectrum))
|
||||
#define GST_IS_STRAWBERRY_FASTSPECTRUM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GST_TYPE_FASTSPECTRUM))
|
||||
#define GST_STRAWBERRY_FASTSPECTRUM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GST_TYPE_FASTSPECTRUM, GstStrawberryFastSpectrumClass))
|
||||
#define GST_IS_STRAWBERRY_FASTSPECTRUM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GST_TYPE_FASTSPECTRUM))
|
||||
#define GST_TYPE_FASTSPECTRUM (gst_fastspectrum_get_type())
|
||||
#define GST_FASTSPECTRUM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GST_TYPE_FASTSPECTRUM, GstFastSpectrum))
|
||||
#define GST_IS_FASTSPECTRUM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GST_TYPE_FASTSPECTRUM))
|
||||
#define GST_FASTSPECTRUM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GST_TYPE_FASTSPECTRUM, GstFastSpectrumClass))
|
||||
#define GST_IS_FASTSPECTRUM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GST_TYPE_FASTSPECTRUM))
|
||||
|
||||
typedef void (*GstStrawberryFastSpectrumInputData)(const guint8 *in, double *out, guint64 len, double max_value, guint op, guint nfft);
|
||||
class QMutex;
|
||||
|
||||
using GstStrawberryFastSpectrumOutputCallback = std::function<void(double *magnitudes, int size)>;
|
||||
typedef void (*GstFastSpectrumInputData)(const guint8 *in, double *out, guint len, double max_value, guint op, guint nfft);
|
||||
|
||||
struct GstStrawberryFastSpectrum {
|
||||
using OutputCallback = std::function<void(double *magnitudes, int size)>;
|
||||
|
||||
struct GstFastSpectrum {
|
||||
GstAudioFilter parent;
|
||||
|
||||
// Properties
|
||||
@@ -75,17 +77,20 @@ struct GstStrawberryFastSpectrum {
|
||||
|
||||
GMutex lock;
|
||||
|
||||
GstStrawberryFastSpectrumInputData input_data;
|
||||
GstStrawberryFastSpectrumOutputCallback output_callback;
|
||||
GstFastSpectrumInputData input_data;
|
||||
|
||||
OutputCallback output_callback;
|
||||
};
|
||||
|
||||
struct GstStrawberryFastSpectrumClass {
|
||||
struct GstFastSpectrumClass {
|
||||
GstAudioFilterClass parent_class;
|
||||
GMutex fftw_lock;
|
||||
|
||||
// Static lock for creating & destroying FFTW plans.
|
||||
QMutex *fftw_lock;
|
||||
};
|
||||
|
||||
GType gst_strawberry_fastspectrum_get_type(void);
|
||||
GType gst_fastspectrum_get_type(void);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif // GST_STRAWBERRY_FASTSPECTRUM_H
|
||||
#endif // GST_MOODBAR_FASTSPECTRUM_H
|
||||
46
ext/gstmoodbar/gstmoodbarplugin.cpp
Normal file
46
ext/gstmoodbar/gstmoodbarplugin.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <glib.h>
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include "gstfastspectrum.h"
|
||||
#include "gstmoodbarplugin.h"
|
||||
|
||||
static gboolean gst_moodbar_plugin_init(GstPlugin *plugin) {
|
||||
|
||||
if (!gst_element_register(plugin, "fastspectrum", GST_RANK_NONE, GST_TYPE_FASTSPECTRUM)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int gstfastspectrum_register_static() {
|
||||
|
||||
return gst_plugin_register_static(
|
||||
GST_VERSION_MAJOR,
|
||||
GST_VERSION_MINOR,
|
||||
"fastspectrum",
|
||||
"Fast spectrum analyzer for generating Moodbars",
|
||||
gst_moodbar_plugin_init,
|
||||
"0.1",
|
||||
"GPL",
|
||||
"FastSpectrum",
|
||||
"FastSpectrum",
|
||||
"https://www.strawberrymusicplayer.org");
|
||||
}
|
||||
25
ext/gstmoodbar/gstmoodbarplugin.h
Normal file
25
ext/gstmoodbar/gstmoodbarplugin.h
Normal file
@@ -0,0 +1,25 @@
|
||||
/* This file was part of Clementine.
|
||||
Copyright 2014, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 GST_MOODBAR_PLUGIN_H
|
||||
#define GST_MOODBAR_PLUGIN_H
|
||||
|
||||
extern "C" {
|
||||
int gstfastspectrum_register_static();
|
||||
}
|
||||
|
||||
#endif // GST_MOODBAR_PLUGIN_H
|
||||
43
ext/libstrawberry-common/CMakeLists.txt
Normal file
43
ext/libstrawberry-common/CMakeLists.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(SOURCES
|
||||
core/logging.cpp
|
||||
core/messagehandler.cpp
|
||||
core/messagereply.cpp
|
||||
core/workerpool.cpp
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
core/logging.h
|
||||
core/messagehandler.h
|
||||
core/messagereply.h
|
||||
core/workerpool.h
|
||||
)
|
||||
|
||||
qt_wrap_cpp(MOC ${HEADERS})
|
||||
|
||||
add_library(libstrawberry-common STATIC ${SOURCES} ${MOC})
|
||||
|
||||
target_include_directories(libstrawberry-common SYSTEM PRIVATE ${GLIB_INCLUDE_DIRS})
|
||||
target_include_directories(libstrawberry-common PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}
|
||||
${CMAKE_BINARY_DIR}/src
|
||||
)
|
||||
|
||||
if(Backtrace_FOUND)
|
||||
target_include_directories(libstrawberry-common SYSTEM PRIVATE ${Backtrace_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
target_link_directories(libstrawberry-common PRIVATE ${GLIB_LIBRARY_DIRS})
|
||||
|
||||
target_link_libraries(libstrawberry-common PRIVATE
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
${GLIB_LIBRARIES}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::Network
|
||||
)
|
||||
|
||||
if(Backtrace_FOUND)
|
||||
target_link_libraries(libstrawberry-common PRIVATE ${Backtrace_LIBRARIES})
|
||||
endif()
|
||||
@@ -61,8 +61,6 @@
|
||||
|
||||
#include "logging.h"
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace logging {
|
||||
|
||||
static Level sDefaultLevel = Level_Debug;
|
||||
@@ -115,10 +113,7 @@ class DebugBase : public QDebug {
|
||||
class BufferedDebug : public DebugBase<BufferedDebug> {
|
||||
public:
|
||||
BufferedDebug() = default;
|
||||
explicit BufferedDebug(QtMsgType msg_type) : buf_(new QBuffer, later_deleter) {
|
||||
|
||||
Q_UNUSED(msg_type)
|
||||
|
||||
explicit BufferedDebug(QtMsgType) : buf_(new QBuffer, later_deleter) {
|
||||
buf_->open(QIODevice::WriteOnly);
|
||||
|
||||
// QDebug doesn't have a method to set a new io device, but swap() allows the devices to be swapped between two instances.
|
||||
@@ -140,9 +135,7 @@ class LoggedDebug : public DebugBase<LoggedDebug> {
|
||||
explicit LoggedDebug(QtMsgType t) : DebugBase(t) { nospace() << kMessageHandlerMagic; }
|
||||
};
|
||||
|
||||
static void MessageHandler(QtMsgType type, const QMessageLogContext &message_log_context, const QString &message) {
|
||||
|
||||
Q_UNUSED(message_log_context)
|
||||
static void MessageHandler(QtMsgType type, const QMessageLogContext&, const QString &message) {
|
||||
|
||||
if (message.startsWith(QLatin1String(kMessageHandlerMagic))) {
|
||||
QByteArray message_data = message.toUtf8();
|
||||
@@ -166,9 +159,9 @@ static void MessageHandler(QtMsgType type, const QMessageLogContext &message_log
|
||||
break;
|
||||
}
|
||||
|
||||
const QStringList lines = message.split(u'\n');
|
||||
const QStringList lines = message.split(QLatin1Char('\n'));
|
||||
for (const QString &line : lines) {
|
||||
BufferedDebug d = CreateLogger<BufferedDebug>(level, u"unknown"_s, -1, nullptr);
|
||||
BufferedDebug d = CreateLogger<BufferedDebug>(level, QStringLiteral("unknown"), -1, nullptr);
|
||||
d << line.toLocal8Bit().constData();
|
||||
if (d.buf_) {
|
||||
d.buf_->close();
|
||||
@@ -203,9 +196,9 @@ void SetLevels(const QString &levels) {
|
||||
|
||||
if (!sClassLevels) return;
|
||||
|
||||
const QStringList items = levels.split(u',');
|
||||
const QStringList items = levels.split(QLatin1Char(','));
|
||||
for (const QString &item : items) {
|
||||
const QStringList class_level = item.split(u':');
|
||||
const QStringList class_level = item.split(QLatin1Char(':'));
|
||||
|
||||
QString class_name;
|
||||
bool ok = false;
|
||||
@@ -223,7 +216,7 @@ void SetLevels(const QString &levels) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (class_name.isEmpty() || class_name == u'*') {
|
||||
if (class_name.isEmpty() || class_name == QLatin1Char('*')) {
|
||||
sDefaultLevel = static_cast<Level>(level);
|
||||
}
|
||||
else {
|
||||
@@ -237,9 +230,9 @@ static QString ParsePrettyFunction(const char *pretty_function) {
|
||||
|
||||
// Get the class name out of the function name.
|
||||
QString class_name = QLatin1String(pretty_function);
|
||||
const qint64 paren = class_name.indexOf(u'(');
|
||||
const qint64 paren = class_name.indexOf(QLatin1Char('('));
|
||||
if (paren != -1) {
|
||||
const qint64 colons = class_name.lastIndexOf("::"_L1, paren);
|
||||
const qint64 colons = class_name.lastIndexOf(QLatin1String("::"), paren);
|
||||
if (colons != -1) {
|
||||
class_name = class_name.left(colons);
|
||||
}
|
||||
@@ -248,7 +241,7 @@ static QString ParsePrettyFunction(const char *pretty_function) {
|
||||
}
|
||||
}
|
||||
|
||||
const qint64 space = class_name.lastIndexOf(u' ');
|
||||
const qint64 space = class_name.lastIndexOf(QLatin1Char(' '));
|
||||
if (space != -1) {
|
||||
class_name = class_name.mid(space + 1);
|
||||
}
|
||||
@@ -295,7 +288,7 @@ static T CreateLogger(Level level, const QString &class_name, int line, const ch
|
||||
}
|
||||
|
||||
T ret(type);
|
||||
ret.nospace() << QDateTime::currentDateTime().toString(u"hh:mm:ss.zzz"_s).toLatin1().constData() << level_name << function_line.leftJustified(32).toLatin1().constData();
|
||||
ret.nospace() << QDateTime::currentDateTime().toString(QStringLiteral("hh:mm:ss.zzz")).toLatin1().constData() << level_name << function_line.leftJustified(32).toLatin1().constData();
|
||||
|
||||
return ret.space();
|
||||
|
||||
@@ -321,7 +314,7 @@ QString CXXDemangle(const QString &mangled_function) {
|
||||
QString LinuxDemangle(const QString &symbol);
|
||||
QString LinuxDemangle(const QString &symbol) {
|
||||
|
||||
static const QRegularExpression regex_symbol(u"\\(([^+]+)"_s);
|
||||
static const QRegularExpression regex_symbol(QStringLiteral("\\(([^+]+)"));
|
||||
QRegularExpressionMatch match = regex_symbol.match(symbol);
|
||||
if (!match.hasMatch()) {
|
||||
return symbol;
|
||||
@@ -336,7 +329,11 @@ QString LinuxDemangle(const QString &symbol) {
|
||||
QString DarwinDemangle(const QString &symbol);
|
||||
QString DarwinDemangle(const QString &symbol) {
|
||||
|
||||
const QStringList split = symbol.split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
# if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QStringList split = symbol.split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
# else
|
||||
QStringList split = symbol.split(QLatin1Char(' '), QString::SkipEmptyParts);
|
||||
# endif
|
||||
QString mangled_function = split[3];
|
||||
return CXXDemangle(mangled_function);
|
||||
|
||||
117
ext/libstrawberry-common/core/messagehandler.cpp
Normal file
117
ext/libstrawberry-common/core/messagehandler.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#include "messagehandler.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QAbstractSocket>
|
||||
#include <QDataStream>
|
||||
#include <QIODevice>
|
||||
#include <QLocalSocket>
|
||||
#include <QByteArray>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
_MessageHandlerBase::_MessageHandlerBase(QIODevice *device, QObject *parent)
|
||||
: QObject(parent),
|
||||
device_(nullptr),
|
||||
flush_abstract_socket_(nullptr),
|
||||
flush_local_socket_(nullptr),
|
||||
reading_protobuf_(false),
|
||||
expected_length_(0),
|
||||
is_device_closed_(false) {
|
||||
if (device) {
|
||||
SetDevice(device);
|
||||
}
|
||||
}
|
||||
|
||||
void _MessageHandlerBase::SetDevice(QIODevice *device) {
|
||||
|
||||
device_ = device;
|
||||
|
||||
buffer_.open(QIODevice::ReadWrite);
|
||||
|
||||
QObject::connect(device, &QIODevice::readyRead, this, &_MessageHandlerBase::DeviceReadyRead);
|
||||
|
||||
// Yeah I know.
|
||||
if (QAbstractSocket *abstractsocket = qobject_cast<QAbstractSocket*>(device)) {
|
||||
flush_abstract_socket_ = &QAbstractSocket::flush;
|
||||
QObject::connect(abstractsocket, &QAbstractSocket::disconnected, this, &_MessageHandlerBase::DeviceClosed);
|
||||
}
|
||||
else if (QLocalSocket *localsocket = qobject_cast<QLocalSocket*>(device)) {
|
||||
flush_local_socket_ = &QLocalSocket::flush;
|
||||
QObject::connect(localsocket, &QLocalSocket::disconnected, this, &_MessageHandlerBase::DeviceClosed);
|
||||
}
|
||||
else {
|
||||
qFatal("Unsupported device type passed to _MessageHandlerBase");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void _MessageHandlerBase::DeviceReadyRead() {
|
||||
|
||||
while (device_->bytesAvailable() > 0) {
|
||||
if (!reading_protobuf_) {
|
||||
// Read the length of the next message
|
||||
QDataStream s(device_);
|
||||
s >> expected_length_;
|
||||
|
||||
reading_protobuf_ = true;
|
||||
}
|
||||
|
||||
// Read some of the message
|
||||
buffer_.write(device_->read(expected_length_ - buffer_.size()));
|
||||
|
||||
// Did we get everything?
|
||||
if (buffer_.size() == expected_length_) {
|
||||
// Parse the message
|
||||
if (!RawMessageArrived(buffer_.data())) {
|
||||
qLog(Error) << "Malformed protobuf message";
|
||||
device_->close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the buffer
|
||||
buffer_.close();
|
||||
buffer_.setData(QByteArray());
|
||||
buffer_.open(QIODevice::ReadWrite);
|
||||
reading_protobuf_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void _MessageHandlerBase::WriteMessage(const QByteArray &data) {
|
||||
|
||||
QDataStream s(device_);
|
||||
s << static_cast<quint32>(data.length());
|
||||
s.writeRawData(data.data(), static_cast<int>(data.length()));
|
||||
|
||||
// Sorry.
|
||||
if (flush_abstract_socket_) {
|
||||
((qobject_cast<QAbstractSocket*>(device_))->*(flush_abstract_socket_))();
|
||||
}
|
||||
else if (flush_local_socket_) {
|
||||
((qobject_cast<QLocalSocket*>(device_))->*(flush_local_socket_))();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void _MessageHandlerBase::DeviceClosed() {
|
||||
is_device_closed_ = true;
|
||||
AbortAll();
|
||||
}
|
||||
176
ext/libstrawberry-common/core/messagehandler.h
Normal file
176
ext/libstrawberry-common/core/messagehandler.h
Normal file
@@ -0,0 +1,176 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
#ifndef MESSAGEHANDLER_H
|
||||
#define MESSAGEHANDLER_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QBuffer>
|
||||
#include <QByteArray>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QLocalSocket>
|
||||
#include <QAbstractSocket>
|
||||
|
||||
#include "core/messagereply.h"
|
||||
|
||||
class QIODevice;
|
||||
|
||||
// Reads and writes uint32 length encoded protobufs to a socket.
|
||||
// This base QObject is separate from AbstractMessageHandler because moc can't handle templated classes.
|
||||
// Use AbstractMessageHandler instead.
|
||||
class _MessageHandlerBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// device can be nullptr, in which case you must call SetDevice before writing any messages.
|
||||
_MessageHandlerBase(QIODevice *device, QObject *parent);
|
||||
|
||||
void SetDevice(QIODevice *device);
|
||||
|
||||
// After this is true, messages cannot be sent to the handler any more.
|
||||
bool is_device_closed() const { return is_device_closed_; }
|
||||
|
||||
protected Q_SLOTS:
|
||||
void WriteMessage(const QByteArray &data);
|
||||
void DeviceReadyRead();
|
||||
virtual void DeviceClosed();
|
||||
|
||||
protected:
|
||||
virtual bool RawMessageArrived(const QByteArray &data) = 0;
|
||||
virtual void AbortAll() = 0;
|
||||
|
||||
protected:
|
||||
typedef bool (QAbstractSocket::*FlushAbstractSocket)();
|
||||
typedef bool (QLocalSocket::*FlushLocalSocket)();
|
||||
|
||||
QIODevice *device_;
|
||||
FlushAbstractSocket flush_abstract_socket_;
|
||||
FlushLocalSocket flush_local_socket_;
|
||||
|
||||
bool reading_protobuf_;
|
||||
quint32 expected_length_;
|
||||
QBuffer buffer_;
|
||||
|
||||
bool is_device_closed_;
|
||||
};
|
||||
|
||||
// Reads and writes uint32 length encoded MessageType messages to a socket.
|
||||
// You should subclass this and implement the MessageArrived(MessageType) method.
|
||||
template<typename MT>
|
||||
class AbstractMessageHandler : public _MessageHandlerBase {
|
||||
public:
|
||||
AbstractMessageHandler(QIODevice *device, QObject *parent);
|
||||
~AbstractMessageHandler() override { AbstractMessageHandler::AbortAll(); }
|
||||
|
||||
using MessageType = MT;
|
||||
using ReplyType = MessageReply<MT>;
|
||||
|
||||
// Serialises the message and writes it to the socket.
|
||||
// This version MUST be called from the thread in which the AbstractMessageHandler was created.
|
||||
void SendMessage(const MessageType &message);
|
||||
|
||||
// Serialises the message and writes it to the socket.
|
||||
// This version may be called from any thread.
|
||||
void SendMessageAsync(const MessageType &message);
|
||||
|
||||
// Sends the request message inside and takes ownership of the MessageReply.
|
||||
// The MessageReply's Finished() signal will be emitted when a reply arrives with the same ID. Must be called from my thread.
|
||||
void SendRequest(ReplyType *reply);
|
||||
|
||||
// Sets the "id" field of reply to the same as the request, and sends the reply on the socket. Used on the worker side.
|
||||
void SendReply(const MessageType &request, MessageType *reply);
|
||||
|
||||
protected:
|
||||
// Called when a message is received from the socket.
|
||||
virtual void MessageArrived(const MessageType &message) { Q_UNUSED(message); }
|
||||
|
||||
// _MessageHandlerBase
|
||||
bool RawMessageArrived(const QByteArray &data) override;
|
||||
void AbortAll() override;
|
||||
|
||||
private:
|
||||
QMap<int, ReplyType*> pending_replies_;
|
||||
};
|
||||
|
||||
template<typename MT>
|
||||
AbstractMessageHandler<MT>::AbstractMessageHandler(QIODevice *device, QObject *parent)
|
||||
: _MessageHandlerBase(device, parent) {}
|
||||
|
||||
template<typename MT>
|
||||
void AbstractMessageHandler<MT>::SendMessage(const MessageType &message) {
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
const std::string data = message.SerializeAsString();
|
||||
WriteMessage(QByteArray(data.data(), data.size()));
|
||||
}
|
||||
|
||||
template<typename MT>
|
||||
void AbstractMessageHandler<MT>::SendMessageAsync(const MessageType &message) {
|
||||
const std::string data = message.SerializeAsString();
|
||||
QMetaObject::invokeMethod(this, "WriteMessage", Qt::QueuedConnection, Q_ARG(QByteArray, QByteArray(data.data(), data.size())));
|
||||
}
|
||||
|
||||
template<typename MT>
|
||||
void AbstractMessageHandler<MT>::SendRequest(ReplyType *reply) {
|
||||
pending_replies_[reply->id()] = reply;
|
||||
SendMessage(reply->request_message());
|
||||
}
|
||||
|
||||
template<typename MT>
|
||||
void AbstractMessageHandler<MT>::SendReply(const MessageType &request, MessageType *reply) {
|
||||
reply->set_id(request.id());
|
||||
SendMessage(*reply);
|
||||
}
|
||||
|
||||
template<typename MT>
|
||||
bool AbstractMessageHandler<MT>::RawMessageArrived(const QByteArray &data) {
|
||||
|
||||
MessageType message;
|
||||
if (!message.ParseFromArray(data.constData(), data.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pending_replies_.contains(message.id())) {
|
||||
// This is a reply to a message that we created earlier.
|
||||
ReplyType *reply = pending_replies_.take(message.id());
|
||||
reply->SetReply(message);
|
||||
}
|
||||
else {
|
||||
MessageArrived(message);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
template<typename MT>
|
||||
void AbstractMessageHandler<MT>::AbortAll() {
|
||||
|
||||
for (ReplyType *reply : pending_replies_) {
|
||||
reply->Abort();
|
||||
}
|
||||
pending_replies_.clear();
|
||||
|
||||
}
|
||||
|
||||
#endif // MESSAGEHANDLER_H
|
||||
48
ext/libstrawberry-common/core/messagereply.cpp
Normal file
48
ext/libstrawberry-common/core/messagereply.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, 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 "messagereply.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
_MessageReplyBase::_MessageReplyBase(QObject *parent)
|
||||
: QObject(parent), finished_(false), success_(false) {}
|
||||
|
||||
bool _MessageReplyBase::WaitForFinished() {
|
||||
|
||||
qLog(Debug) << "Waiting on ID" << id();
|
||||
semaphore_.acquire();
|
||||
qLog(Debug) << "Acquired ID" << id();
|
||||
return success_;
|
||||
|
||||
}
|
||||
|
||||
void _MessageReplyBase::Abort() {
|
||||
|
||||
Q_ASSERT(!finished_);
|
||||
finished_ = true;
|
||||
success_ = false;
|
||||
|
||||
Q_EMIT Finished();
|
||||
qLog(Debug) << "Releasing ID" << id() << "(aborted)";
|
||||
semaphore_.release();
|
||||
|
||||
}
|
||||
99
ext/libstrawberry-common/core/messagereply.h
Normal file
99
ext/libstrawberry-common/core/messagereply.h
Normal file
@@ -0,0 +1,99 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, 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 MESSAGEREPLY_H
|
||||
#define MESSAGEREPLY_H
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QSemaphore>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
// Base QObject for a reply future class that is returned immediately for requests that will occur in the background.
|
||||
// Similar to QNetworkReply. Use MessageReply instead.
|
||||
class _MessageReplyBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit _MessageReplyBase(QObject *parent = nullptr);
|
||||
|
||||
virtual int id() const = 0;
|
||||
bool is_finished() const { return finished_; }
|
||||
bool is_successful() const { return success_; }
|
||||
|
||||
// Waits for the reply to finish by waiting on a semaphore. Never call this from the MessageHandler's thread or it will block forever.
|
||||
// Returns true if the call was successful.
|
||||
bool WaitForFinished();
|
||||
|
||||
void Abort();
|
||||
|
||||
Q_SIGNALS:
|
||||
void Finished();
|
||||
|
||||
protected:
|
||||
bool finished_;
|
||||
bool success_;
|
||||
|
||||
QSemaphore semaphore_;
|
||||
};
|
||||
|
||||
// A reply future class that is returned immediately for requests that will occur in the background. Similar to QNetworkReply.
|
||||
template<typename MessageType>
|
||||
class MessageReply : public _MessageReplyBase {
|
||||
public:
|
||||
explicit MessageReply(const MessageType &request_message, QObject *parent = nullptr);
|
||||
|
||||
int id() const override { return request_message_.id(); }
|
||||
const MessageType &request_message() const { return request_message_; }
|
||||
const MessageType &message() const { return reply_message_; }
|
||||
|
||||
void SetReply(const MessageType &message);
|
||||
|
||||
private:
|
||||
MessageType request_message_;
|
||||
MessageType reply_message_;
|
||||
};
|
||||
|
||||
|
||||
template<typename MessageType>
|
||||
MessageReply<MessageType>::MessageReply(const MessageType &request_message, QObject *parent) : _MessageReplyBase(parent) {
|
||||
request_message_.MergeFrom(request_message);
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
void MessageReply<MessageType>::SetReply(const MessageType &message) {
|
||||
|
||||
Q_ASSERT(!finished_);
|
||||
|
||||
reply_message_.MergeFrom(message);
|
||||
finished_ = true;
|
||||
success_ = true;
|
||||
|
||||
qLog(Debug) << "Releasing ID" << id() << "(finished)";
|
||||
|
||||
// Delay the signal as workaround to fix the signal periodically not emitted.
|
||||
QTimer::singleShot(1, this, &_MessageReplyBase::Finished);
|
||||
|
||||
semaphore_.release();
|
||||
|
||||
}
|
||||
|
||||
#endif // MESSAGEREPLY_H
|
||||
23
ext/libstrawberry-common/core/workerpool.cpp
Normal file
23
ext/libstrawberry-common/core/workerpool.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, 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 <QObject>
|
||||
|
||||
#include "workerpool.h"
|
||||
|
||||
_WorkerPoolBase::_WorkerPoolBase(QObject *parent) : QObject(parent) {}
|
||||
466
ext/libstrawberry-common/core/workerpool.h
Normal file
466
ext/libstrawberry-common/core/workerpool.h
Normal file
@@ -0,0 +1,466 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, 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 WORKERPOOL_H
|
||||
#define WORKERPOOL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QMutex>
|
||||
#include <QLocalServer>
|
||||
#include <QProcess>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QList>
|
||||
#include <QQueue>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QAtomicInt>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
class QLocalSocket;
|
||||
|
||||
// Base class containing signals and slots - required because moc doesn't do templated objects.
|
||||
class _WorkerPoolBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit _WorkerPoolBase(QObject *parent = nullptr);
|
||||
|
||||
Q_SIGNALS:
|
||||
// Emitted when a worker failed to start. This usually happens when the worker wasn't found, or couldn't be executed.
|
||||
void WorkerFailedToStart();
|
||||
|
||||
protected Q_SLOTS:
|
||||
virtual void DoStart() {}
|
||||
virtual void NewConnection() {}
|
||||
virtual void ProcessReadyReadStandardOutput() {}
|
||||
virtual void ProcessReadyReadStandardError() {}
|
||||
virtual void ProcessError(QProcess::ProcessError) {}
|
||||
virtual void SendQueuedMessages() {}
|
||||
};
|
||||
|
||||
|
||||
// Manages a pool of one or more external processes.
|
||||
// A local socket server is started for each process, and the address is passed to the process as argv[1].
|
||||
// The process is expected to connect back to the socket server, and when it does a HandlerType is created for it.
|
||||
// Instances of HandlerType are created in the WorkerPool's thread.
|
||||
template<typename HandlerType>
|
||||
class WorkerPool : public _WorkerPoolBase {
|
||||
public:
|
||||
explicit WorkerPool(QObject *parent = nullptr);
|
||||
~WorkerPool() override;
|
||||
|
||||
using MessageType = typename HandlerType::MessageType;
|
||||
using ReplyType = typename HandlerType::ReplyType;
|
||||
|
||||
// Sets the name of the worker executable. This is looked for first in the current directory, and then in $PATH.
|
||||
// You must call this before calling Start().
|
||||
void SetExecutableName(const QString &executable_name);
|
||||
|
||||
// Sets the number of worker process to use. Defaults to 1 <= (processors / 2) <= 2.
|
||||
void SetWorkerCount(const int count);
|
||||
|
||||
// Sets the prefix to use for the local server (on unix this is a named pipe in /tmp).
|
||||
// Defaults to QApplication::applicationName().
|
||||
// A random number is appended to this name when creating each server.
|
||||
void SetLocalServerName(const QString &local_server_name);
|
||||
|
||||
// Starts all workers.
|
||||
void Start();
|
||||
|
||||
// Fills in the message's "id" field and creates a reply future.
|
||||
// The message is queued and the WorkerPool's thread will send it to the next available worker.
|
||||
// Can be called from any thread.
|
||||
ReplyType *SendMessageWithReply(MessageType *message);
|
||||
|
||||
protected:
|
||||
// These are all reimplemented slots, they are called on the WorkerPool's thread.
|
||||
void DoStart() override;
|
||||
void NewConnection() override;
|
||||
void ProcessReadyReadStandardOutput() override;
|
||||
void ProcessReadyReadStandardError() override;
|
||||
void ProcessError(QProcess::ProcessError error) override;
|
||||
void SendQueuedMessages() override;
|
||||
|
||||
private:
|
||||
struct Worker {
|
||||
Worker() : local_server_(nullptr), local_socket_(nullptr), process_(nullptr), handler_(nullptr) {}
|
||||
|
||||
QLocalServer *local_server_;
|
||||
QLocalSocket *local_socket_;
|
||||
QProcess *process_;
|
||||
HandlerType *handler_;
|
||||
};
|
||||
|
||||
// Must only ever be called on my thread.
|
||||
void StartOneWorker(Worker *worker);
|
||||
|
||||
template<typename T>
|
||||
Worker *FindWorker(T Worker::*member, T value) {
|
||||
for (typename QList<Worker>::iterator it = workers_.begin(); it != workers_.end(); ++it) {
|
||||
if ((*it).*member == value) {
|
||||
return &(*it);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void DeleteQObjectPointerLater(T **p) {
|
||||
if (*p) {
|
||||
(*p)->deleteLater();
|
||||
*p = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a new reply future for the request with the next sequential ID,
|
||||
// and sets the request's ID to the ID of the reply. Can be called from any thread
|
||||
ReplyType *NewReply(MessageType *message);
|
||||
|
||||
// Returns the next handler, or nullptr if there isn't one. Must be called from my thread.
|
||||
HandlerType *NextHandler() const;
|
||||
|
||||
private:
|
||||
QString local_server_name_;
|
||||
QString executable_name_;
|
||||
QString executable_path_;
|
||||
|
||||
int worker_count_;
|
||||
mutable int next_worker_;
|
||||
QList<Worker> workers_;
|
||||
|
||||
QAtomicInt next_id_;
|
||||
|
||||
QMutex message_queue_mutex_;
|
||||
QQueue<ReplyType *> message_queue_;
|
||||
};
|
||||
|
||||
|
||||
template<typename HandlerType>
|
||||
WorkerPool<HandlerType>::WorkerPool(QObject *parent)
|
||||
: _WorkerPoolBase(parent),
|
||||
worker_count_(1),
|
||||
next_worker_(0),
|
||||
next_id_(0) {
|
||||
|
||||
local_server_name_ = qApp->applicationName().toLower();
|
||||
|
||||
if (local_server_name_.isEmpty()) {
|
||||
local_server_name_ = QStringLiteral("workerpool");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
WorkerPool<HandlerType>::~WorkerPool() {
|
||||
|
||||
for (const Worker &worker : workers_) {
|
||||
if (worker.local_socket_ && worker.process_) {
|
||||
QObject::disconnect(worker.process_, &QProcess::errorOccurred, this, &WorkerPool::ProcessError);
|
||||
QObject::disconnect(worker.process_, &QProcess::readyReadStandardOutput, this, &WorkerPool::ProcessReadyReadStandardOutput);
|
||||
QObject::disconnect(worker.process_, &QProcess::readyReadStandardError, this, &WorkerPool::ProcessReadyReadStandardError);
|
||||
|
||||
// The worker is connected. Close his socket and wait for him to exit.
|
||||
qLog(Debug) << "Closing worker socket";
|
||||
worker.local_socket_->close();
|
||||
worker.process_->waitForFinished(500);
|
||||
}
|
||||
|
||||
if (worker.process_ && worker.process_->state() == QProcess::Running) {
|
||||
// The worker is still running - kill it.
|
||||
qLog(Debug) << "Killing worker process";
|
||||
worker.process_->terminate();
|
||||
if (!worker.process_->waitForFinished(500)) {
|
||||
worker.process_->kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ReplyType *reply : message_queue_) {
|
||||
reply->Abort();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SetWorkerCount(const int count) {
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
worker_count_ = count;
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SetLocalServerName(const QString &local_server_name) {
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
local_server_name_ = local_server_name;
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SetExecutableName(const QString &executable_name) {
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
executable_name_ = executable_name;
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::Start() {
|
||||
QMetaObject::invokeMethod(this, &WorkerPool<HandlerType>::DoStart);
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::DoStart() {
|
||||
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
Q_ASSERT(!executable_name_.isEmpty());
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
// Find the executable if we can, default to searching $PATH
|
||||
executable_path_ = executable_name_;
|
||||
|
||||
QStringList search_path;
|
||||
search_path << QCoreApplication::applicationDirPath();
|
||||
#if defined(Q_OS_UNIX)
|
||||
search_path << QStringLiteral("/usr/libexec");
|
||||
search_path << QStringLiteral("/usr/local/libexec");
|
||||
#endif
|
||||
#if defined(Q_OS_MACOS)
|
||||
search_path << QDir::cleanPath(QCoreApplication::applicationDirPath() + QStringLiteral("/../PlugIns"));
|
||||
#endif
|
||||
|
||||
for (const QString &path_prefix : std::as_const(search_path)) {
|
||||
const QString executable_path = path_prefix + QLatin1Char('/') + executable_name_;
|
||||
if (QFile::exists(executable_path)) {
|
||||
executable_path_ = executable_path;
|
||||
qLog(Debug) << "Using worker" << executable_name_ << "from" << path_prefix;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (executable_path_ == executable_name_) {
|
||||
qLog(Debug) << "Using worker" << executable_name_;
|
||||
}
|
||||
|
||||
// Start all the workers
|
||||
for (int i = 0; i < worker_count_; ++i) {
|
||||
Worker worker;
|
||||
StartOneWorker(&worker);
|
||||
|
||||
workers_ << worker;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::StartOneWorker(Worker *worker) {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
DeleteQObjectPointerLater(&worker->local_server_);
|
||||
DeleteQObjectPointerLater(&worker->local_socket_);
|
||||
DeleteQObjectPointerLater(&worker->process_);
|
||||
DeleteQObjectPointerLater(&worker->handler_);
|
||||
|
||||
worker->local_server_ = new QLocalServer(this);
|
||||
worker->process_ = new QProcess(this);
|
||||
|
||||
QObject::connect(worker->local_server_, &QLocalServer::newConnection, this, &WorkerPool::NewConnection);
|
||||
QObject::connect(worker->process_, &QProcess::errorOccurred, this, &WorkerPool::ProcessError);
|
||||
QObject::connect(worker->process_, &QProcess::readyReadStandardOutput, this, &WorkerPool::ProcessReadyReadStandardOutput);
|
||||
QObject::connect(worker->process_, &QProcess::readyReadStandardError, this, &WorkerPool::ProcessReadyReadStandardError);
|
||||
|
||||
// Create a server, find an unused name and start listening
|
||||
Q_FOREVER {
|
||||
const quint32 unique_number = QRandomGenerator::global()->bounded(static_cast<quint32>(quint64(this) & 0xFFFFFFFF));
|
||||
const QString name = QStringLiteral("%1_%2").arg(local_server_name_).arg(unique_number);
|
||||
|
||||
if (worker->local_server_->listen(name)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
qLog(Debug) << "Starting worker" << worker << executable_path_ << worker->local_server_->fullServerName();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
worker->process_->setProcessChannelMode(QProcess::SeparateChannels);
|
||||
#else
|
||||
worker->process_->setProcessChannelMode(QProcess::ForwardedChannels);
|
||||
#endif
|
||||
|
||||
worker->process_->start(executable_path_, QStringList() << worker->local_server_->fullServerName());
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::NewConnection() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
QLocalServer *server = qobject_cast<QLocalServer*>(sender());
|
||||
|
||||
// Find the worker with this server.
|
||||
Worker *worker = FindWorker(&Worker::local_server_, server);
|
||||
if (!worker) return;
|
||||
|
||||
qLog(Debug) << "Worker" << worker << "connected to" << server->fullServerName();
|
||||
|
||||
// Accept the connection.
|
||||
worker->local_socket_ = server->nextPendingConnection();
|
||||
|
||||
// We only ever accept one connection per worker, so destroy the server now.
|
||||
worker->local_socket_->setParent(this);
|
||||
worker->local_server_->deleteLater();
|
||||
worker->local_server_ = nullptr;
|
||||
|
||||
// Create the handler.
|
||||
worker->handler_ = new HandlerType(worker->local_socket_, this);
|
||||
|
||||
SendQueuedMessages();
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::ProcessError(QProcess::ProcessError error) {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
QProcess *process = qobject_cast<QProcess*>(sender());
|
||||
|
||||
// Find the worker with this process.
|
||||
Worker *worker = FindWorker(&Worker::process_, process);
|
||||
if (!worker) return;
|
||||
|
||||
switch (error) {
|
||||
case QProcess::FailedToStart:
|
||||
// Failed to start errors are bad - it usually means the worker isn't installed.
|
||||
// Don't restart the process, but tell our owner, who will probably want to do something fatal.
|
||||
qLog(Error) << "Worker failed to start";
|
||||
Q_EMIT WorkerFailedToStart();
|
||||
break;
|
||||
|
||||
default:
|
||||
// On any other error we just restart the process.
|
||||
qLog(Debug) << "Worker" << worker << "failed with error" << error << "- restarting";
|
||||
StartOneWorker(worker);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::ProcessReadyReadStandardOutput() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
QProcess *process = qobject_cast<QProcess*>(sender());
|
||||
QByteArray data = process->readAllStandardOutput();
|
||||
|
||||
fprintf(stdout, "%s", data.data());
|
||||
fflush(stdout);
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::ProcessReadyReadStandardError() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
QProcess *process = qobject_cast<QProcess*>(sender());
|
||||
QByteArray data = process->readAllStandardError();
|
||||
|
||||
fprintf(stderr, "%s", data.data());
|
||||
fflush(stderr);
|
||||
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
typename WorkerPool<HandlerType>::ReplyType*
|
||||
WorkerPool<HandlerType>::NewReply(MessageType *message) {
|
||||
|
||||
const int id = next_id_.fetchAndAddOrdered(1);
|
||||
message->set_id(id);
|
||||
|
||||
return new ReplyType(*message);
|
||||
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
typename WorkerPool<HandlerType>::ReplyType*
|
||||
WorkerPool<HandlerType>::SendMessageWithReply(MessageType *message) {
|
||||
|
||||
ReplyType *reply = NewReply(message);
|
||||
|
||||
// Add the pending reply to the queue
|
||||
{
|
||||
QMutexLocker l(&message_queue_mutex_);
|
||||
message_queue_.enqueue(reply);
|
||||
}
|
||||
|
||||
// Wake up the main thread
|
||||
QMetaObject::invokeMethod(this, &WorkerPool<HandlerType>::SendQueuedMessages, Qt::QueuedConnection);
|
||||
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SendQueuedMessages() {
|
||||
|
||||
QMutexLocker l(&message_queue_mutex_);
|
||||
|
||||
while (!message_queue_.isEmpty()) {
|
||||
ReplyType *reply = message_queue_.dequeue();
|
||||
|
||||
// Find a worker for this message
|
||||
HandlerType *handler = NextHandler();
|
||||
if (!handler) {
|
||||
// No available handlers - put the message on the front of the queue.
|
||||
message_queue_.prepend(reply);
|
||||
qLog(Debug) << "No available handlers to process request";
|
||||
break;
|
||||
}
|
||||
|
||||
handler->SendRequest(reply);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<typename HandlerType>
|
||||
HandlerType *WorkerPool<HandlerType>::NextHandler() const {
|
||||
|
||||
for (int i = 0; i < workers_.count(); ++i) {
|
||||
const int worker_index = (next_worker_ + i) % workers_.count();
|
||||
|
||||
if (workers_[worker_index].handler_ && !workers_[worker_index].handler_->is_device_closed()) {
|
||||
next_worker_ = (worker_index + 1) % workers_.count();
|
||||
return workers_[worker_index].handler_;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
#endif // WORKERPOOL_H
|
||||
63
ext/libstrawberry-tagreader/CMakeLists.txt
Normal file
63
ext/libstrawberry-tagreader/CMakeLists.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Workaround a bug in protobuf-generate.cmake (https://github.com/protocolbuffers/protobuf/issues/12450)
|
||||
if(NOT protobuf_PROTOC_EXE)
|
||||
set(protobuf_PROTOC_EXE "protobuf::protoc")
|
||||
endif()
|
||||
|
||||
if(NOT Protobuf_LIBRARIES)
|
||||
set(Protobuf_LIBRARIES protobuf::libprotobuf)
|
||||
endif()
|
||||
|
||||
set(SOURCES tagreaderbase.cpp tagreadermessages.proto)
|
||||
|
||||
if(HAVE_TAGLIB)
|
||||
list(APPEND SOURCES tagreadertaglib.cpp tagreadergme.cpp)
|
||||
endif()
|
||||
|
||||
if(HAVE_TAGPARSER)
|
||||
list(APPEND SOURCES tagreadertagparser.cpp)
|
||||
endif()
|
||||
|
||||
add_library(libstrawberry-tagreader STATIC ${PROTO_SOURCES} ${SOURCES})
|
||||
|
||||
target_include_directories(libstrawberry-tagreader SYSTEM PRIVATE
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
${PROTOBUF_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_include_directories(libstrawberry-tagreader PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${CMAKE_SOURCE_DIR}/ext/libstrawberry-common
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_BINARY_DIR}/src
|
||||
)
|
||||
|
||||
target_link_directories(libstrawberry-tagreader PRIVATE
|
||||
${GLIB_LIBRARY_DIRS}
|
||||
${PROTOBUF_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE
|
||||
${GLIB_LIBRARIES}
|
||||
${Protobuf_LIBRARIES}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::Network
|
||||
Qt${QT_VERSION_MAJOR}::Gui
|
||||
libstrawberry-common
|
||||
)
|
||||
|
||||
if(HAVE_TAGLIB)
|
||||
target_include_directories(libstrawberry-tagreader SYSTEM PRIVATE ${TAGLIB_INCLUDE_DIRS})
|
||||
target_link_directories(libstrawberry-tagreader PRIVATE ${TAGLIB_LIBRARY_DIRS})
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE ${TAGLIB_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(HAVE_TAGPARSER)
|
||||
target_include_directories(libstrawberry-tagreader SYSTEM PRIVATE ${TAGPARSER_INCLUDE_DIRS})
|
||||
target_link_directories(libstrawberry-tagreader PRIVATE ${TAGPARSER_LIBRARY_DIRS})
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE ${TAGPARSER_LIBRARIES})
|
||||
endif()
|
||||
|
||||
protobuf_generate(TARGET libstrawberry-tagreader)
|
||||
171
ext/libstrawberry-tagreader/tagreaderbase.cpp
Normal file
171
ext/libstrawberry-tagreader/tagreaderbase.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QIODevice>
|
||||
#include <QFile>
|
||||
#include <QBuffer>
|
||||
#include <QImage>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "tagreaderbase.h"
|
||||
|
||||
TagReaderBase::TagReaderBase() = default;
|
||||
TagReaderBase::~TagReaderBase() = default;
|
||||
|
||||
QString TagReaderBase::ErrorString(const Result &result) {
|
||||
|
||||
switch (result.error_code) {
|
||||
case Result::ErrorCode::Success:
|
||||
return QObject::tr("Success");
|
||||
case Result::ErrorCode::Unsupported:
|
||||
return QObject::tr("File is unsupported");
|
||||
case Result::ErrorCode::FilenameMissing:
|
||||
return QObject::tr("Filename is missing");
|
||||
case Result::ErrorCode::FileDoesNotExist:
|
||||
return QObject::tr("File does not exist");
|
||||
case Result::ErrorCode::FileOpenError:
|
||||
return QObject::tr("File could not be opened");
|
||||
case Result::ErrorCode::FileParseError:
|
||||
return QObject::tr("Could not parse file");
|
||||
case Result::ErrorCode::FileSaveError:
|
||||
return QObject::tr("Could save file");
|
||||
case Result::ErrorCode::CustomError:
|
||||
return result.error;
|
||||
}
|
||||
|
||||
return QObject::tr("Unknown error");
|
||||
|
||||
}
|
||||
|
||||
float TagReaderBase::ConvertPOPMRating(const int POPM_rating) {
|
||||
|
||||
if (POPM_rating < 0x01) return 0.0F;
|
||||
if (POPM_rating < 0x40) return 0.20F;
|
||||
if (POPM_rating < 0x80) return 0.40F;
|
||||
if (POPM_rating < 0xC0) return 0.60F;
|
||||
if (POPM_rating < 0xFC) return 0.80F;
|
||||
|
||||
return 1.0F;
|
||||
|
||||
}
|
||||
|
||||
int TagReaderBase::ConvertToPOPMRating(const float rating) {
|
||||
|
||||
if (rating < 0.20) return 0x00;
|
||||
if (rating < 0.40) return 0x01;
|
||||
if (rating < 0.60) return 0x40;
|
||||
if (rating < 0.80) return 0x80;
|
||||
if (rating < 1.0) return 0xC0;
|
||||
|
||||
return 0xFF;
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const QString &song_filename, const spb::tagreader::WriteFileRequest &request) {
|
||||
|
||||
if (!request.has_save_cover() || !request.save_cover()) {
|
||||
return Cover();
|
||||
}
|
||||
|
||||
QString cover_filename;
|
||||
if (request.has_cover_filename()) {
|
||||
cover_filename = QString::fromStdString(request.cover_filename());
|
||||
}
|
||||
QByteArray cover_data;
|
||||
if (request.has_cover_data()) {
|
||||
cover_data = QByteArray(request.cover_data().data(), static_cast<qint64>(request.cover_data().size()));
|
||||
}
|
||||
QString cover_mime_type;
|
||||
if (request.has_cover_mime_type()) {
|
||||
cover_mime_type = QString::fromStdString(request.cover_mime_type());
|
||||
}
|
||||
|
||||
return LoadCoverFromRequest(song_filename, cover_filename, cover_data, cover_mime_type);
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const QString &song_filename, const spb::tagreader::SaveEmbeddedArtRequest &request) {
|
||||
|
||||
QString cover_filename;
|
||||
if (request.has_cover_filename()) {
|
||||
cover_filename = QString::fromStdString(request.cover_filename());
|
||||
}
|
||||
QByteArray cover_data;
|
||||
if (request.has_cover_data()) {
|
||||
cover_data = QByteArray(request.cover_data().data(), static_cast<qint64>(request.cover_data().size()));
|
||||
}
|
||||
QString cover_mime_type;
|
||||
if (request.has_cover_mime_type()) {
|
||||
cover_mime_type = QString::fromStdString(request.cover_mime_type());
|
||||
}
|
||||
|
||||
return LoadCoverFromRequest(song_filename, cover_filename, cover_data, cover_mime_type);
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, QString cover_mime_type) {
|
||||
|
||||
if (cover_data.isEmpty() && !cover_filename.isEmpty()) {
|
||||
qLog(Debug) << "Loading cover from" << cover_filename << "for" << song_filename;
|
||||
QFile file(cover_filename);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qLog(Error) << "Failed to open file" << cover_filename << "for reading:" << file.errorString();
|
||||
return Cover();
|
||||
}
|
||||
cover_data = file.readAll();
|
||||
file.close();
|
||||
}
|
||||
|
||||
if (!cover_data.isEmpty()) {
|
||||
if (cover_mime_type.isEmpty()) {
|
||||
cover_mime_type = QMimeDatabase().mimeTypeForData(cover_data).name();
|
||||
}
|
||||
if (cover_mime_type == QLatin1String("image/jpeg")) {
|
||||
qLog(Debug) << "Using cover from JPEG data for" << song_filename;
|
||||
return Cover(cover_data, cover_mime_type);
|
||||
}
|
||||
if (cover_mime_type == QLatin1String("image/png")) {
|
||||
qLog(Debug) << "Using cover from PNG data for" << song_filename;
|
||||
return Cover(cover_data, cover_mime_type);
|
||||
}
|
||||
// Convert image to JPEG.
|
||||
qLog(Debug) << "Converting cover to JPEG data for" << song_filename;
|
||||
QImage cover_image;
|
||||
if (!cover_image.loadFromData(cover_data)) {
|
||||
qLog(Error) << "Failed to load image from cover data for" << song_filename;
|
||||
return Cover();
|
||||
}
|
||||
cover_data.clear();
|
||||
QBuffer buffer(&cover_data);
|
||||
if (buffer.open(QIODevice::WriteOnly)) {
|
||||
cover_image.save(&buffer, "JPEG");
|
||||
buffer.close();
|
||||
}
|
||||
return Cover(cover_data, QStringLiteral("image/jpeg"));
|
||||
}
|
||||
|
||||
return Cover();
|
||||
|
||||
}
|
||||
91
ext/libstrawberry-tagreader/tagreaderbase.h
Normal file
91
ext/libstrawberry-tagreader/tagreaderbase.h
Normal file
@@ -0,0 +1,91 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERBASE_H
|
||||
#define TAGREADERBASE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
/*
|
||||
* This class holds all useful methods to read and write tags from/to files.
|
||||
* You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient)
|
||||
*/
|
||||
class TagReaderBase {
|
||||
public:
|
||||
explicit TagReaderBase();
|
||||
virtual ~TagReaderBase();
|
||||
|
||||
class Result {
|
||||
public:
|
||||
enum class ErrorCode {
|
||||
Success,
|
||||
Unsupported,
|
||||
FilenameMissing,
|
||||
FileDoesNotExist,
|
||||
FileOpenError,
|
||||
FileParseError,
|
||||
FileSaveError,
|
||||
CustomError,
|
||||
};
|
||||
Result(const ErrorCode _error_code, const QString &_error = QString()) : error_code(_error_code), error(_error) {}
|
||||
ErrorCode error_code;
|
||||
QString error;
|
||||
bool success() const { return error_code == ErrorCode::Success; }
|
||||
};
|
||||
|
||||
class Cover {
|
||||
public:
|
||||
explicit Cover(const QByteArray &_data = QByteArray(), const QString &_mime_type = QString()) : data(_data), mime_type(_mime_type) {}
|
||||
QByteArray data;
|
||||
QString mime_type;
|
||||
QString error;
|
||||
};
|
||||
|
||||
static QString ErrorString(const Result &result);
|
||||
|
||||
virtual bool IsMediaFile(const QString &filename) const = 0;
|
||||
|
||||
virtual Result ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0;
|
||||
virtual Result WriteFile(const QString &filename, const spb::tagreader::WriteFileRequest &request) const = 0;
|
||||
|
||||
virtual Result LoadEmbeddedArt(const QString &filename, QByteArray &data) const = 0;
|
||||
virtual Result SaveEmbeddedArt(const QString &filename, const spb::tagreader::SaveEmbeddedArtRequest &request) const = 0;
|
||||
|
||||
virtual Result SaveSongPlaycountToFile(const QString &filename, const uint playcount) const = 0;
|
||||
virtual Result SaveSongRatingToFile(const QString &filename, const float rating) const = 0;
|
||||
|
||||
protected:
|
||||
static float ConvertPOPMRating(const int POPM_rating);
|
||||
static int ConvertToPOPMRating(const float rating);
|
||||
|
||||
static Cover LoadCoverFromRequest(const QString &song_filename, const spb::tagreader::WriteFileRequest &request);
|
||||
static Cover LoadCoverFromRequest(const QString &song_filename, const spb::tagreader::SaveEmbeddedArtRequest &request);
|
||||
|
||||
private:
|
||||
static Cover LoadCoverFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, QString cover_mime_type);
|
||||
|
||||
Q_DISABLE_COPY(TagReaderBase)
|
||||
};
|
||||
|
||||
#endif // TAGREADERBASE_H
|
||||
@@ -28,30 +28,29 @@
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
|
||||
#include "constants/timeconstants.h"
|
||||
#include "utilities/timeconstants.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/messagehandler.h"
|
||||
#include "tagreaderbase.h"
|
||||
#include "tagreadertaglib.h"
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
#undef TStringToQString
|
||||
#undef QStringToTString
|
||||
|
||||
bool GME::IsSupportedFormat(const QFileInfo &fileinfo) {
|
||||
return fileinfo.exists() && (fileinfo.completeSuffix().endsWith("spc"_L1, Qt::CaseInsensitive) || fileinfo.completeSuffix().endsWith("vgm"_L1), Qt::CaseInsensitive);
|
||||
return fileinfo.exists() && (fileinfo.completeSuffix().endsWith(QLatin1String("spc"), Qt::CaseInsensitive) || fileinfo.completeSuffix().endsWith(QLatin1String("vgm")), Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
TagReaderResult GME::ReadFile(const QFileInfo &fileinfo, Song *song) {
|
||||
TagReaderBase::Result GME::ReadFile(const QFileInfo &fileinfo, spb::tagreader::SongMetadata *song) {
|
||||
|
||||
if (fileinfo.completeSuffix().endsWith("spc"_L1), Qt::CaseInsensitive) {
|
||||
if (fileinfo.completeSuffix().endsWith(QLatin1String("spc")), Qt::CaseInsensitive) {
|
||||
return SPC::Read(fileinfo, song);
|
||||
}
|
||||
if (fileinfo.completeSuffix().endsWith("vgm"_L1, Qt::CaseInsensitive)) {
|
||||
if (fileinfo.completeSuffix().endsWith(QLatin1String("vgm"), Qt::CaseInsensitive)) {
|
||||
return VGM::Read(fileinfo, song);
|
||||
}
|
||||
|
||||
return TagReaderResult::ErrorCode::Unsupported;
|
||||
return TagReaderBase::Result::ErrorCode::Unsupported;
|
||||
|
||||
}
|
||||
|
||||
@@ -68,18 +67,18 @@ quint32 GME::UnpackBytes32(const char *const bytes, size_t length) {
|
||||
|
||||
}
|
||||
|
||||
TagReaderResult GME::SPC::Read(const QFileInfo &fileinfo, Song *song) {
|
||||
TagReaderBase::Result GME::SPC::Read(const QFileInfo &fileinfo, spb::tagreader::SongMetadata *song) {
|
||||
|
||||
QFile file(fileinfo.filePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return TagReaderResult(TagReaderResult::ErrorCode::FileOpenError, file.errorString());
|
||||
return TagReaderBase::Result(TagReaderBase::Result::ErrorCode::FileOpenError, file.errorString());
|
||||
}
|
||||
|
||||
qLog(Debug) << "Reading tags from SPC file" << fileinfo.fileName();
|
||||
|
||||
// Check for header -- more reliable than file name alone.
|
||||
if (!file.read(33).startsWith("SNES-SPC700")) {
|
||||
return TagReaderResult::ErrorCode::Unsupported;
|
||||
if (!file.read(33).startsWith(QStringLiteral("SNES-SPC700").toLatin1())) {
|
||||
return TagReaderBase::Result::ErrorCode::Unsupported;
|
||||
}
|
||||
|
||||
// First order of business -- get any tag values that exist within the core file information.
|
||||
@@ -112,7 +111,7 @@ TagReaderResult GME::SPC::Read(const QFileInfo &fileinfo, Song *song) {
|
||||
}
|
||||
|
||||
if (length_in_sec < 0x1FFF) {
|
||||
song->set_length_nanosec(static_cast<qint64>(length_in_sec * kNsecPerSec));
|
||||
song->set_length_nanosec(length_in_sec * kNsecPerSec);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +129,7 @@ TagReaderResult GME::SPC::Read(const QFileInfo &fileinfo, Song *song) {
|
||||
// Check for XID6 data -- this is infrequently used, but being able to fill in data from this is ideal before trying to rely on APETAG values.
|
||||
// XID6 format follows EA's binary file format standard named "IFF"
|
||||
file.seek(XID6_OFFSET);
|
||||
if (has_id6 && file.read(4) == "xid6") {
|
||||
if (has_id6 && file.read(4) == QStringLiteral("xid6").toLatin1()) {
|
||||
QByteArray xid6_head_data = file.read(4);
|
||||
if (xid6_head_data.size() >= 4) {
|
||||
qint64 xid6_size = xid6_head_data[0] | (xid6_head_data[1] << 8) | (xid6_head_data[2] << 16) | xid6_head_data[3];
|
||||
@@ -161,21 +160,21 @@ TagReaderResult GME::SPC::Read(const QFileInfo &fileinfo, Song *song) {
|
||||
if (ape.hasAPETag()) {
|
||||
TagLib::Tag *tag = ape.tag();
|
||||
if (!tag) {
|
||||
return TagReaderResult::ErrorCode::FileParseError;
|
||||
return TagReaderBase::Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
song->set_artist(tag->artist());
|
||||
song->set_album(tag->album());
|
||||
song->set_title(tag->title());
|
||||
song->set_genre(tag->genre());
|
||||
TagReaderTagLib::AssignTagLibStringToStdString(tag->artist(), song->mutable_artist());
|
||||
TagReaderTagLib::AssignTagLibStringToStdString(tag->album(), song->mutable_album());
|
||||
TagReaderTagLib::AssignTagLibStringToStdString(tag->title(), song->mutable_title());
|
||||
TagReaderTagLib::AssignTagLibStringToStdString(tag->genre(), song->mutable_genre());
|
||||
song->set_track(static_cast<std::int32_t>(tag->track()));
|
||||
song->set_year(static_cast<std::int32_t>(tag->year()));
|
||||
}
|
||||
|
||||
song->set_valid(true);
|
||||
song->set_filetype(Song::FileType::SPC);
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType_SPC);
|
||||
|
||||
return TagReaderResult::ErrorCode::Success;
|
||||
return TagReaderBase::Result::ErrorCode::Success;
|
||||
|
||||
}
|
||||
|
||||
@@ -197,23 +196,23 @@ quint64 GME::SPC::ConvertSPCStringToNum(const QByteArray &arr) {
|
||||
|
||||
}
|
||||
|
||||
TagReaderResult GME::VGM::Read(const QFileInfo &fileinfo, Song *song) {
|
||||
TagReaderBase::Result GME::VGM::Read(const QFileInfo &fileinfo, spb::tagreader::SongMetadata *song) {
|
||||
|
||||
QFile file(fileinfo.filePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return TagReaderResult(TagReaderResult::ErrorCode::FileOpenError, file.errorString());
|
||||
return TagReaderBase::Result(TagReaderBase::Result::ErrorCode::FileOpenError, file.errorString());
|
||||
}
|
||||
|
||||
qLog(Debug) << "Reading tags from VGM file" << fileinfo.filePath();
|
||||
|
||||
if (!file.read(4).startsWith("Vgm ")) {
|
||||
return TagReaderResult::ErrorCode::Unsupported;
|
||||
if (!file.read(4).startsWith(QStringLiteral("Vgm ").toLatin1())) {
|
||||
return TagReaderBase::Result::ErrorCode::Unsupported;
|
||||
}
|
||||
|
||||
file.seek(GD3_TAG_PTR);
|
||||
QByteArray gd3_head = file.read(4);
|
||||
if (gd3_head.size() < 4) {
|
||||
return TagReaderResult::ErrorCode::FileParseError;
|
||||
return TagReaderBase::Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
quint64 pt = GME::UnpackBytes32(gd3_head.constData(), gd3_head.size());
|
||||
@@ -225,7 +224,7 @@ TagReaderResult GME::VGM::Read(const QFileInfo &fileinfo, Song *song) {
|
||||
quint64 length = 0;
|
||||
|
||||
if (!GetPlaybackLength(sample_count_bytes, loop_count_bytes, length)) {
|
||||
return TagReaderResult::ErrorCode::FileParseError;
|
||||
return TagReaderBase::Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
file.seek(static_cast<qint64>(GD3_TAG_PTR + pt));
|
||||
@@ -239,10 +238,14 @@ TagReaderResult GME::VGM::Read(const QFileInfo &fileinfo, Song *song) {
|
||||
QByteArray gd3Data = file.read(gd3_length);
|
||||
QTextStream fileTagStream(gd3Data, QIODevice::ReadOnly);
|
||||
// Stored as 16 bit UTF string, two bytes per letter.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
fileTagStream.setEncoding(QStringConverter::Utf16);
|
||||
QStringList strings = fileTagStream.readLine(0).split(u'\0');
|
||||
#else
|
||||
fileTagStream.setCodec("UTF-16");
|
||||
#endif
|
||||
QStringList strings = fileTagStream.readLine(0).split(QLatin1Char('\0'));
|
||||
if (strings.count() < 10) {
|
||||
return TagReaderResult::ErrorCode::FileParseError;
|
||||
return TagReaderBase::Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
// VGM standard dictates string tag data exist in specific order.
|
||||
@@ -252,11 +255,11 @@ TagReaderResult GME::VGM::Read(const QFileInfo &fileinfo, Song *song) {
|
||||
song->set_album(strings[2].toStdString());
|
||||
song->set_artist(strings[6].toStdString());
|
||||
song->set_year(strings[8].left(4).toInt());
|
||||
song->set_length_nanosec(static_cast<qint64>(length * kNsecPerMsec));
|
||||
song->set_length_nanosec(length * kNsecPerMsec);
|
||||
song->set_valid(true);
|
||||
song->set_filetype(Song::FileType::VGM);
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType_VGM);
|
||||
|
||||
return TagReaderResult::ErrorCode::Success;
|
||||
return TagReaderBase::Result::ErrorCode::Success;
|
||||
|
||||
}
|
||||
|
||||
@@ -287,63 +290,61 @@ bool GME::VGM::GetPlaybackLength(const QByteArray &sample_count_bytes, const QBy
|
||||
TagReaderGME::TagReaderGME() = default;
|
||||
|
||||
|
||||
TagReaderResult TagReaderGME::IsMediaFile(const QString &filename) const {
|
||||
bool TagReaderGME::IsMediaFile(const QString &filename) const {
|
||||
|
||||
QFileInfo fileinfo(filename);
|
||||
return GME::IsSupportedFormat(fileinfo) ? TagReaderResult::ErrorCode::Success : TagReaderResult::ErrorCode::Unsupported;
|
||||
return GME::IsSupportedFormat(fileinfo);
|
||||
|
||||
}
|
||||
|
||||
TagReaderResult TagReaderGME::ReadFile(const QString &filename, Song *song) const {
|
||||
TagReaderBase::Result TagReaderGME::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
|
||||
QFileInfo fileinfo(filename);
|
||||
return GME::ReadFile(fileinfo, song);
|
||||
|
||||
}
|
||||
|
||||
TagReaderResult TagReaderGME::WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const {
|
||||
TagReaderBase::Result TagReaderGME::WriteFile(const QString &filename, const spb::tagreader::WriteFileRequest &request) const {
|
||||
|
||||
Q_UNUSED(filename);
|
||||
Q_UNUSED(song);
|
||||
Q_UNUSED(save_tags_options);
|
||||
Q_UNUSED(save_tag_cover_data);
|
||||
Q_UNUSED(request);
|
||||
|
||||
return TagReaderResult::ErrorCode::Unsupported;
|
||||
return Result::ErrorCode::Unsupported;
|
||||
|
||||
}
|
||||
|
||||
TagReaderResult TagReaderGME::LoadEmbeddedCover(const QString &filename, QByteArray &data) const {
|
||||
TagReaderBase::Result TagReaderGME::LoadEmbeddedArt(const QString &filename, QByteArray &data) const {
|
||||
|
||||
Q_UNUSED(filename);
|
||||
Q_UNUSED(data);
|
||||
|
||||
return TagReaderResult::ErrorCode::Unsupported;
|
||||
return Result::ErrorCode::Unsupported;
|
||||
|
||||
}
|
||||
|
||||
TagReaderResult TagReaderGME::SaveEmbeddedCover(const QString &filename, const SaveTagCoverData &save_tag_cover_data) const {
|
||||
TagReaderBase::Result TagReaderGME::SaveEmbeddedArt(const QString &filename, const spb::tagreader::SaveEmbeddedArtRequest &request) const {
|
||||
|
||||
Q_UNUSED(filename);
|
||||
Q_UNUSED(save_tag_cover_data);
|
||||
Q_UNUSED(request);
|
||||
|
||||
return TagReaderResult::ErrorCode::Unsupported;
|
||||
return Result::ErrorCode::Unsupported;
|
||||
|
||||
}
|
||||
|
||||
TagReaderResult TagReaderGME::SaveSongPlaycount(const QString &filename, const uint playcount) const {
|
||||
TagReaderBase::Result TagReaderGME::SaveSongPlaycountToFile(const QString &filename, const uint playcount) const {
|
||||
|
||||
Q_UNUSED(filename);
|
||||
Q_UNUSED(playcount);
|
||||
|
||||
return TagReaderResult::ErrorCode::Unsupported;
|
||||
return Result::ErrorCode::Unsupported;
|
||||
|
||||
}
|
||||
|
||||
TagReaderResult TagReaderGME::SaveSongRating(const QString &filename, const float rating) const {
|
||||
TagReaderBase::Result TagReaderGME::SaveSongRatingToFile(const QString &filename, const float rating) const {
|
||||
|
||||
Q_UNUSED(filename);
|
||||
Q_UNUSED(rating);
|
||||
|
||||
return TagReaderResult::ErrorCode::Unsupported;
|
||||
return Result::ErrorCode::Unsupported;
|
||||
|
||||
}
|
||||
@@ -25,10 +25,11 @@
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "tagreaderbase.h"
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
namespace GME {
|
||||
bool IsSupportedFormat(const QFileInfo &fileinfo);
|
||||
TagReaderResult ReadFile(const QFileInfo &fileinfo, Song *song);
|
||||
TagReaderBase::Result ReadFile(const QFileInfo &fileinfo, spb::tagreader::SongMetadata *song);
|
||||
|
||||
uint32_t UnpackBytes32(const char *const bytes, size_t length);
|
||||
|
||||
@@ -68,7 +69,7 @@ enum class xID6_TYPE {
|
||||
Integer = 0x4
|
||||
};
|
||||
|
||||
TagReaderResult Read(const QFileInfo &fileinfo, Song *song);
|
||||
TagReaderBase::Result Read(const QFileInfo &fileinfo, spb::tagreader::SongMetadata *song);
|
||||
qint16 GetNextMemAddressAlign32bit(qint16 input);
|
||||
quint64 ConvertSPCStringToNum(const QByteArray &arr);
|
||||
} // namespace SPC
|
||||
@@ -84,7 +85,7 @@ constexpr int LOOP_SAMPLE_COUNT = 0x20;
|
||||
constexpr int SAMPLE_TIMEBASE = 44100;
|
||||
constexpr int GST_GME_LOOP_TIME_MS = 8000;
|
||||
|
||||
TagReaderResult Read(const QFileInfo &fileinfo, Song *song);
|
||||
TagReaderBase::Result Read(const QFileInfo &fileinfo, spb::tagreader::SongMetadata *song);
|
||||
// Takes in two QByteArrays, expected to be 4 bytes long. Desired length is returned via output parameter out_length. Returns false on error.
|
||||
bool GetPlaybackLength(const QByteArray &sample_count_bytes, const QByteArray &loop_count_bytes, quint64 &out_length);
|
||||
|
||||
@@ -99,16 +100,16 @@ class TagReaderGME : public TagReaderBase {
|
||||
public:
|
||||
explicit TagReaderGME();
|
||||
|
||||
TagReaderResult IsMediaFile(const QString &filename) const override;
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
TagReaderResult ReadFile(const QString &filename, Song *song) const override;
|
||||
TagReaderResult WriteFile(const QString &filename, const Song &song, const SaveTagsOptions save_tags_options, const SaveTagCoverData &save_tag_cover_data) const override;
|
||||
Result ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
Result WriteFile(const QString &filename, const spb::tagreader::WriteFileRequest &request) const override;
|
||||
|
||||
TagReaderResult LoadEmbeddedCover(const QString &filename, QByteArray &data) const override;
|
||||
TagReaderResult SaveEmbeddedCover(const QString &filename, const SaveTagCoverData &save_tag_cover_data) const override;
|
||||
Result LoadEmbeddedArt(const QString &filename, QByteArray &data) const override;
|
||||
Result SaveEmbeddedArt(const QString &filename, const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
|
||||
|
||||
TagReaderResult SaveSongPlaycount(const QString &filename, const uint playcount) const override;
|
||||
TagReaderResult SaveSongRating(const QString &filename, const float rating) const override;
|
||||
Result SaveSongPlaycountToFile(const QString &filename, const uint playcount) const override;
|
||||
Result SaveSongRatingToFile(const QString &filename, const float rating) const override;
|
||||
};
|
||||
|
||||
#endif // TAGREADERGME_H
|
||||
195
ext/libstrawberry-tagreader/tagreadermessages.proto
Normal file
195
ext/libstrawberry-tagreader/tagreadermessages.proto
Normal file
@@ -0,0 +1,195 @@
|
||||
syntax = "proto2";
|
||||
|
||||
package spb.tagreader;
|
||||
|
||||
message SongMetadata {
|
||||
|
||||
enum FileType {
|
||||
UNKNOWN = 0;
|
||||
WAV = 1;
|
||||
FLAC = 2;
|
||||
WAVPACK = 3;
|
||||
OGGFLAC = 4;
|
||||
OGGVORBIS = 5;
|
||||
OGGOPUS = 6;
|
||||
OGGSPEEX = 7;
|
||||
MPEG = 8;
|
||||
MP4 = 9;
|
||||
ASF = 10;
|
||||
AIFF = 11;
|
||||
MPC = 12;
|
||||
TRUEAUDIO = 13;
|
||||
DSF = 14;
|
||||
DSDIFF = 15;
|
||||
PCM = 16;
|
||||
APE = 17;
|
||||
MOD = 18;
|
||||
S3M = 19;
|
||||
XM = 20;
|
||||
IT = 21;
|
||||
SPC = 22;
|
||||
VGM = 23;
|
||||
CDDA = 90;
|
||||
STREAM = 91;
|
||||
}
|
||||
|
||||
optional bool valid = 1;
|
||||
|
||||
optional string title = 2;
|
||||
optional string album = 3;
|
||||
optional string artist = 4;
|
||||
optional string albumartist = 5;
|
||||
optional int32 track = 6;
|
||||
optional int32 disc = 7;
|
||||
optional int32 year = 8;
|
||||
optional int32 originalyear = 9;
|
||||
optional string genre = 10;
|
||||
optional bool compilation = 11;
|
||||
optional string composer = 12;
|
||||
optional string performer = 13;
|
||||
optional string grouping = 14;
|
||||
optional string comment = 15;
|
||||
optional string lyrics = 16;
|
||||
|
||||
optional uint64 length_nanosec = 17;
|
||||
|
||||
optional int32 bitrate = 18;
|
||||
optional int32 samplerate = 19;
|
||||
optional int32 bitdepth = 20;
|
||||
|
||||
optional string url = 21;
|
||||
optional string basefilename = 22;
|
||||
optional FileType filetype = 23;
|
||||
optional int64 filesize = 24;
|
||||
optional int64 mtime = 25;
|
||||
optional int64 ctime = 26;
|
||||
|
||||
optional uint32 playcount = 27;
|
||||
optional uint32 skipcount = 28;
|
||||
optional int64 lastplayed = 29;
|
||||
optional int64 lastseen = 30;
|
||||
|
||||
optional bool art_embedded = 31;
|
||||
|
||||
optional float rating = 32;
|
||||
|
||||
optional string acoustid_id = 33;
|
||||
optional string acoustid_fingerprint = 34;
|
||||
|
||||
optional string musicbrainz_album_artist_id = 35; // MusicBrainz Release Artist ID (MUSICBRAINZ_ALBUMARTISTID)
|
||||
optional string musicbrainz_artist_id = 36; // MusicBrainz Artist ID (MUSICBRAINZ_ARTISTID)
|
||||
optional string musicbrainz_original_artist_id = 37; // MusicBrainz Original Artist ID (MUSICBRAINZ_ORIGINALARTISTID)
|
||||
optional string musicbrainz_album_id = 38; // MusicBrainz Release ID (MUSICBRAINZ_ALBUMID)
|
||||
optional string musicbrainz_original_album_id = 39; // MusicBrainz Original Release ID (MUSICBRAINZ_ORIGINALALBUMID)
|
||||
optional string musicbrainz_recording_id = 40; // MusicBrainz Recording ID (MUSICBRAINZ_TRACKID)
|
||||
optional string musicbrainz_track_id = 41; // MusicBrainz Track ID (MUSICBRAINZ_RELEASETRACKID)
|
||||
optional string musicbrainz_disc_id = 42; // MusicBrainz Disc ID (MUSICBRAINZ_DISCID)
|
||||
optional string musicbrainz_release_group_id = 43; // MusicBrainz Release Group ID (MUSICBRAINZ_RELEASEGROUPID)
|
||||
optional string musicbrainz_work_id = 44; // MusicBrainz Work ID (MUSICBRAINZ_WORKID)
|
||||
|
||||
optional bool suspicious_tags = 50;
|
||||
|
||||
}
|
||||
|
||||
message IsMediaFileRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message IsMediaFileResponse {
|
||||
optional bool success = 1;
|
||||
}
|
||||
|
||||
message ReadFileRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message ReadFileResponse {
|
||||
optional bool success = 1;
|
||||
optional SongMetadata metadata = 2;
|
||||
optional string error = 3;
|
||||
}
|
||||
|
||||
message WriteFileRequest {
|
||||
optional string filename = 1;
|
||||
optional bool save_tags = 2;
|
||||
optional bool save_playcount = 3;
|
||||
optional bool save_rating = 4;
|
||||
optional bool save_cover = 5;
|
||||
optional SongMetadata metadata = 6;
|
||||
optional string cover_filename = 7;
|
||||
optional bytes cover_data = 8;
|
||||
optional string cover_mime_type = 9;
|
||||
}
|
||||
|
||||
message WriteFileResponse {
|
||||
optional bool success = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
message LoadEmbeddedArtRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message LoadEmbeddedArtResponse {
|
||||
optional bool success = 1;
|
||||
optional bytes data = 2;
|
||||
optional string error = 3;
|
||||
}
|
||||
|
||||
message SaveEmbeddedArtRequest {
|
||||
optional string filename = 1;
|
||||
optional string cover_filename = 2;
|
||||
optional bytes cover_data = 3;
|
||||
optional string cover_mime_type = 4;
|
||||
}
|
||||
|
||||
message SaveEmbeddedArtResponse {
|
||||
optional bool success = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
message SaveSongPlaycountToFileRequest {
|
||||
optional string filename = 1;
|
||||
optional uint32 playcount = 2;
|
||||
}
|
||||
|
||||
message SaveSongPlaycountToFileResponse {
|
||||
optional bool success = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
message SaveSongRatingToFileRequest {
|
||||
optional string filename = 1;
|
||||
optional float rating = 2;
|
||||
}
|
||||
|
||||
message SaveSongRatingToFileResponse {
|
||||
optional bool success = 1;
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
message Message {
|
||||
optional int32 id = 1;
|
||||
|
||||
optional ReadFileRequest read_file_request = 2;
|
||||
optional ReadFileResponse read_file_response = 3;
|
||||
|
||||
optional WriteFileRequest write_file_request = 4;
|
||||
optional WriteFileResponse write_file_response = 5;
|
||||
|
||||
optional IsMediaFileRequest is_media_file_request = 6;
|
||||
optional IsMediaFileResponse is_media_file_response = 7;
|
||||
|
||||
optional LoadEmbeddedArtRequest load_embedded_art_request = 8;
|
||||
optional LoadEmbeddedArtResponse load_embedded_art_response = 9;
|
||||
|
||||
optional SaveEmbeddedArtRequest save_embedded_art_request = 10;
|
||||
optional SaveEmbeddedArtResponse save_embedded_art_response = 11;
|
||||
|
||||
optional SaveSongPlaycountToFileRequest save_song_playcount_to_file_request = 12;
|
||||
optional SaveSongPlaycountToFileResponse save_song_playcount_to_file_response = 13;
|
||||
|
||||
optional SaveSongRatingToFileRequest save_song_rating_to_file_request = 14;
|
||||
optional SaveSongRatingToFileResponse save_song_rating_to_file_response = 15;
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
147
ext/libstrawberry-tagreader/tagreadertaglib.h
Normal file
147
ext/libstrawberry-tagreader/tagreadertaglib.h
Normal file
@@ -0,0 +1,147 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2013, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERTAGLIB_H
|
||||
#define TAGREADERTAGLIB_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include <taglib/tstring.h>
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/xiphcomment.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/apefile.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
#include <taglib/popularimeterframe.h>
|
||||
#include <taglib/mp4tag.h>
|
||||
#include <taglib/asftag.h>
|
||||
|
||||
#include "tagreaderbase.h"
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
#undef TStringToQString
|
||||
#undef QStringToTString
|
||||
|
||||
class FileRefFactory;
|
||||
|
||||
/*
|
||||
* This class holds all useful methods to read and write tags from/to files.
|
||||
* You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient)
|
||||
*/
|
||||
class TagReaderTagLib : public TagReaderBase {
|
||||
public:
|
||||
explicit TagReaderTagLib();
|
||||
~TagReaderTagLib() override;
|
||||
|
||||
static inline TagLib::String StdStringToTagLibString(const std::string &s) {
|
||||
return TagLib::String(s.c_str(), TagLib::String::UTF8);
|
||||
}
|
||||
|
||||
static inline std::string TagLibStringToStdString(const TagLib::String &s) {
|
||||
return std::string(s.toCString(true), s.length());
|
||||
}
|
||||
|
||||
static inline TagLib::String QStringToTagLibString(const QString &s) {
|
||||
return TagLib::String(s.toUtf8().constData(), TagLib::String::UTF8);
|
||||
}
|
||||
|
||||
static inline QString TagLibStringToQString(const TagLib::String &s) {
|
||||
return QString::fromUtf8((s).toCString(true));
|
||||
}
|
||||
|
||||
static inline void AssignTagLibStringToStdString(const TagLib::String &tstr, std::string *output) {
|
||||
|
||||
const QString qstr = TagLibStringToQString(tstr).trimmed();
|
||||
const QByteArray data = qstr.toUtf8();
|
||||
output->assign(data.constData(), data.size());
|
||||
|
||||
}
|
||||
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
Result ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
Result WriteFile(const QString &filename, const spb::tagreader::WriteFileRequest &request) const override;
|
||||
|
||||
Result LoadEmbeddedArt(const QString &filename, QByteArray &data) const override;
|
||||
Result SaveEmbeddedArt(const QString &filename, const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
|
||||
|
||||
Result SaveSongPlaycountToFile(const QString &filename, const uint playcount) const override;
|
||||
Result SaveSongRatingToFile(const QString &filename, const float rating) const override;
|
||||
|
||||
private:
|
||||
spb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const;
|
||||
|
||||
void ParseID3v2Tags(TagLib::ID3v2::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseVorbisComments(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseAPETags(const TagLib::APE::ItemListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseMP4Tags(TagLib::MP4::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseASFTags(TagLib::ASF::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseASFAttribute(const TagLib::ASF::AttributeListMap &attributes_map, const char *attribute, std::string *str) const;
|
||||
|
||||
void SetID3v2Tag(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetTextFrame(const char *id, const QString &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetTextFrame(const char *id, const std::string &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetUserTextFrame(const QString &description, const QString &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetUserTextFrame(const std::string &description, const std::string &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetUnsyncLyricsFrame(const std::string &value, TagLib::ID3v2::Tag *tag) const;
|
||||
|
||||
void SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comment, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetAPETag(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetASFTag(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const;
|
||||
void SetAsfAttribute(TagLib::ASF::Tag *tag, const char *attribute, const std::string &value) const;
|
||||
void SetAsfAttribute(TagLib::ASF::Tag *tag, const char *attribute, const int value) const;
|
||||
|
||||
QByteArray LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) const;
|
||||
|
||||
static TagLib::ID3v2::PopularimeterFrame *GetPOPMFrameFromTag(TagLib::ID3v2::Tag *tag);
|
||||
|
||||
void SetPlaycount(TagLib::Ogg::XiphComment *vorbis_comment, const uint playcount) const;
|
||||
void SetPlaycount(TagLib::APE::Tag *tag, const uint playcount) const;
|
||||
void SetPlaycount(TagLib::ID3v2::Tag *tag, const uint playcount) const;
|
||||
void SetPlaycount(TagLib::MP4::Tag *tag, const uint playcount) const;
|
||||
void SetPlaycount(TagLib::ASF::Tag *tag, const uint playcount) const;
|
||||
|
||||
void SetRating(TagLib::Ogg::XiphComment *vorbis_comment, const float rating) const;
|
||||
void SetRating(TagLib::APE::Tag *tag, const float rating) const;
|
||||
void SetRating(TagLib::ID3v2::Tag *tag, const float rating) const;
|
||||
void SetRating(TagLib::MP4::Tag *tag, const float rating) const;
|
||||
void SetRating(TagLib::ASF::Tag *tag, const float rating) const;
|
||||
|
||||
void SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *vorbis_comment, const QByteArray &data, const QString &mime_type) const;
|
||||
void SetEmbeddedArt(TagLib::Ogg::XiphComment *vorbis_comment, const QByteArray &data, const QString &mime_type) const;
|
||||
void SetEmbeddedArt(TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const;
|
||||
void SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data, const QString &mime_type) const;
|
||||
|
||||
static TagLib::String TagLibStringListToSlashSeparatedString(const TagLib::StringList &taglib_string_list);
|
||||
|
||||
private:
|
||||
FileRefFactory *factory_;
|
||||
|
||||
Q_DISABLE_COPY(TagReaderTagLib)
|
||||
};
|
||||
|
||||
#endif // TAGREADERTAGLIB_H
|
||||
555
ext/libstrawberry-tagreader/tagreadertagparser.cpp
Normal file
555
ext/libstrawberry-tagreader/tagreadertagparser.cpp
Normal file
@@ -0,0 +1,555 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2021-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "tagreadertagparser.h"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <tagparser/mediafileinfo.h>
|
||||
#include <tagparser/diagnostics.h>
|
||||
#include <tagparser/progressfeedback.h>
|
||||
#include <tagparser/tag.h>
|
||||
#include <tagparser/abstracttrack.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QDateTime>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/messagehandler.h"
|
||||
#include "utilities/timeconstants.h"
|
||||
|
||||
TagReaderTagParser::TagReaderTagParser() = default;
|
||||
|
||||
bool TagReaderTagParser::IsMediaFile(const QString &filename) const {
|
||||
|
||||
qLog(Debug) << "Checking for valid file" << filename;
|
||||
|
||||
QFileInfo fileinfo(filename);
|
||||
if (!fileinfo.exists() || fileinfo.suffix().compare(QLatin1String("bak"), Qt::CaseInsensitive) == 0) return false;
|
||||
|
||||
try {
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
taginfo.open(true);
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
taginfo.parseTracks(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
const auto tracks = taginfo.tracks();
|
||||
for (TagParser::AbstractTrack *track : tracks) {
|
||||
if (track->mediaType() == TagParser::MediaType::Audio) {
|
||||
taginfo.close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
taginfo.close();
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
|
||||
qLog(Debug) << "Reading tags from" << filename;
|
||||
|
||||
const QFileInfo fileinfo(filename);
|
||||
|
||||
if (!fileinfo.exists() || fileinfo.suffix().compare(QLatin1String("bak"), Qt::CaseInsensitive) == 0) return Result::ErrorCode::FileParseError;
|
||||
|
||||
const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
|
||||
const QByteArray basefilename = fileinfo.fileName().toUtf8();
|
||||
|
||||
song->set_basefilename(basefilename.constData(), basefilename.size());
|
||||
song->set_url(url.constData(), url.size());
|
||||
song->set_filesize(fileinfo.size());
|
||||
|
||||
song->set_mtime(fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL);
|
||||
song->set_ctime(fileinfo.birthTime().isValid() ? std::max(fileinfo.birthTime().toSecsSinceEpoch(), 0LL) : fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL);
|
||||
|
||||
if (song->ctime() <= 0) {
|
||||
song->set_ctime(song->mtime());
|
||||
}
|
||||
|
||||
song->set_lastseen(QDateTime::currentDateTime().toSecsSinceEpoch());
|
||||
|
||||
try {
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
taginfo.setPath(filename.toStdWString().toStdString());
|
||||
#else
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
#endif
|
||||
|
||||
taginfo.open(true);
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTracks(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
std::vector<TagParser::AbstractTrack*> tracks = taginfo.tracks();
|
||||
for (TagParser::AbstractTrack *track : tracks) {
|
||||
switch (track->format().general) {
|
||||
case TagParser::GeneralMediaFormat::Flac:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_FLAC);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::WavPack:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_WAVPACK);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::MonkeysAudio:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_APE);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::WindowsMediaAudio:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_ASF);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Vorbis:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_OGGVORBIS);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Opus:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_OGGOPUS);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Speex:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_OGGSPEEX);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Mpeg1Audio:
|
||||
switch (track->format().sub) {
|
||||
case TagParser::SubFormats::Mpeg1Layer3:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_MPEG);
|
||||
break;
|
||||
case TagParser::SubFormats::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Mpc:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_MPC);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Pcm:
|
||||
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_PCM);
|
||||
break;
|
||||
case TagParser::GeneralMediaFormat::Unknown:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
song->set_length_nanosec(track->duration().totalMilliseconds() * kNsecPerMsec);
|
||||
song->set_samplerate(track->samplingFrequency());
|
||||
song->set_bitdepth(track->bitsPerSample());
|
||||
song->set_bitrate(std::max(track->bitrate(), track->maxBitrate()));
|
||||
}
|
||||
|
||||
if (song->filetype() == spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_UNKNOWN) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::Unsupported;
|
||||
}
|
||||
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
song->set_albumartist(tag->value(TagParser::KnownField::AlbumArtist).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_artist(tag->value(TagParser::KnownField::Artist).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_album(tag->value(TagParser::KnownField::Album).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_title(tag->value(TagParser::KnownField::Title).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_genre(tag->value(TagParser::KnownField::Genre).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_composer(tag->value(TagParser::KnownField::Composer).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_performer(tag->value(TagParser::KnownField::Performers).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_grouping(tag->value(TagParser::KnownField::Grouping).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_comment(tag->value(TagParser::KnownField::Comment).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_lyrics(tag->value(TagParser::KnownField::Lyrics).toString(TagParser::TagTextEncoding::Utf8));
|
||||
song->set_year(tag->value(TagParser::KnownField::RecordDate).toInteger());
|
||||
song->set_originalyear(tag->value(TagParser::KnownField::ReleaseDate).toInteger());
|
||||
song->set_track(tag->value(TagParser::KnownField::TrackPosition).toInteger());
|
||||
song->set_disc(tag->value(TagParser::KnownField::DiskPosition).toInteger());
|
||||
if (!tag->value(TagParser::KnownField::Cover).empty() && tag->value(TagParser::KnownField::Cover).dataSize() > 0) {
|
||||
song->set_art_embedded(true);
|
||||
}
|
||||
const float rating = ConvertPOPMRating(tag->value(TagParser::KnownField::Rating));
|
||||
if (song->rating() <= 0 && rating > 0.0 && rating <= 1.0) {
|
||||
song->set_rating(rating);
|
||||
}
|
||||
}
|
||||
|
||||
// Set integer fields to -1 if they're not valid
|
||||
if (song->track() <= 0) { song->set_track(-1); }
|
||||
if (song->disc() <= 0) { song->set_disc(-1); }
|
||||
if (song->year() <= 0) { song->set_year(-1); }
|
||||
if (song->originalyear() <= 0) { song->set_originalyear(-1); }
|
||||
if (song->samplerate() <= 0) { song->set_samplerate(-1); }
|
||||
if (song->bitdepth() <= 0) { song->set_bitdepth(-1); }
|
||||
if (song->bitrate() <= 0) { song->set_bitrate(-1); }
|
||||
if (song->lastplayed() <= 0) { song->set_lastplayed(-1); }
|
||||
|
||||
song->set_valid(true);
|
||||
|
||||
taginfo.close();
|
||||
|
||||
return Result::ErrorCode::Success;
|
||||
|
||||
}
|
||||
catch(...) {
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::WriteFile(const QString &filename, const spb::tagreader::WriteFileRequest &request) const {
|
||||
|
||||
if (request.filename().empty()) return Result::ErrorCode::FilenameMissing;
|
||||
|
||||
const spb::tagreader::SongMetadata &song = request.metadata();
|
||||
const bool save_tags = request.has_save_tags() && request.save_tags();
|
||||
const bool save_playcount = request.has_save_playcount() && request.save_playcount();
|
||||
const bool save_rating = request.has_save_rating() && request.save_rating();
|
||||
const bool save_cover = request.has_save_cover() && request.save_cover();
|
||||
|
||||
QStringList save_tags_options;
|
||||
if (save_tags) {
|
||||
save_tags_options << QStringLiteral("tags");
|
||||
}
|
||||
if (save_playcount) {
|
||||
save_tags_options << QStringLiteral("playcount");
|
||||
}
|
||||
if (save_rating) {
|
||||
save_tags_options << QStringLiteral("rating");
|
||||
}
|
||||
if (save_cover) {
|
||||
save_tags_options << QStringLiteral("embedded cover");
|
||||
}
|
||||
|
||||
qLog(Debug) << "Saving" << save_tags_options.join(QLatin1String(", ")) << "to" << filename;
|
||||
|
||||
const Cover cover = LoadCoverFromRequest(filename, request);
|
||||
|
||||
try {
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
#ifdef Q_OS_WIN32
|
||||
taginfo.setPath(filename.toStdWString().toStdString());
|
||||
#else
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
#endif
|
||||
taginfo.open(false);
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTracks(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
if (taginfo.tags().size() <= 0) {
|
||||
taginfo.createAppropriateTags();
|
||||
}
|
||||
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
if (save_tags) {
|
||||
tag->setValue(TagParser::KnownField::AlbumArtist, TagParser::TagValue(song.albumartist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Artist, TagParser::TagValue(song.artist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Album, TagParser::TagValue(song.album(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Title, TagParser::TagValue(song.title(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Genre, TagParser::TagValue(song.genre(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Composer, TagParser::TagValue(song.composer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Performers, TagParser::TagValue(song.performer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Grouping, TagParser::TagValue(song.grouping(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Comment, TagParser::TagValue(song.comment(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::Lyrics, TagParser::TagValue(song.lyrics(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
|
||||
tag->setValue(TagParser::KnownField::TrackPosition, TagParser::TagValue(song.track()));
|
||||
tag->setValue(TagParser::KnownField::DiskPosition, TagParser::TagValue(song.disc()));
|
||||
tag->setValue(TagParser::KnownField::RecordDate, TagParser::TagValue(song.year()));
|
||||
tag->setValue(TagParser::KnownField::ReleaseDate, TagParser::TagValue(song.originalyear()));
|
||||
}
|
||||
if (save_playcount) {
|
||||
SaveSongPlaycountToFile(tag, song.playcount());
|
||||
}
|
||||
if (save_rating) {
|
||||
SaveSongRatingToFile(tag, song.rating());
|
||||
}
|
||||
if (save_cover) {
|
||||
SaveEmbeddedArt(tag, cover.data);
|
||||
}
|
||||
}
|
||||
|
||||
taginfo.applyChanges(diag, progress);
|
||||
taginfo.close();
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
return Result::ErrorCode::Success;
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
return Result::ErrorCode::FileParseError;
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::LoadEmbeddedArt(const QString &filename, QByteArray &data) const {
|
||||
|
||||
if (filename.isEmpty()) return Result::ErrorCode::FilenameMissing;
|
||||
|
||||
qLog(Debug) << "Loading art from" << filename;
|
||||
|
||||
try {
|
||||
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
taginfo.setPath(filename.toStdWString().toStdString());
|
||||
#else
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
#endif
|
||||
|
||||
taginfo.open();
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
if (!tag->value(TagParser::KnownField::Cover).empty() && tag->value(TagParser::KnownField::Cover).dataSize() > 0) {
|
||||
data = QByteArray(tag->value(TagParser::KnownField::Cover).dataPointer(), tag->value(TagParser::KnownField::Cover).dataSize());
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::Success;
|
||||
}
|
||||
}
|
||||
|
||||
taginfo.close();
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
return Result::ErrorCode::FileParseError;
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagParser::SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const {
|
||||
|
||||
tag->setValue(TagParser::KnownField::Cover, TagParser::TagValue(data.toStdString()));
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const spb::tagreader::SaveEmbeddedArtRequest &request) const {
|
||||
|
||||
if (request.filename().empty()) return Result::ErrorCode::FilenameMissing;
|
||||
|
||||
qLog(Debug) << "Saving art to" << filename;
|
||||
|
||||
const Cover cover = LoadCoverFromRequest(filename, request);
|
||||
|
||||
try {
|
||||
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
taginfo.setPath(filename.toStdWString().toStdString());
|
||||
#else
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
#endif
|
||||
|
||||
taginfo.open();
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
if (taginfo.tags().size() <= 0) {
|
||||
taginfo.createAppropriateTags();
|
||||
}
|
||||
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
SaveEmbeddedArt(tag, cover.data);
|
||||
}
|
||||
|
||||
taginfo.applyChanges(diag, progress);
|
||||
taginfo.close();
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
return Result::ErrorCode::Success;
|
||||
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
return Result::ErrorCode::FileParseError;
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagParser::SaveSongPlaycountToFile(TagParser::Tag *tag, const uint playcount) const {
|
||||
|
||||
Q_UNUSED(tag);
|
||||
Q_UNUSED(playcount);
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::SaveSongPlaycountToFile(const QString &filename, const uint playcount) const {
|
||||
|
||||
Q_UNUSED(filename);
|
||||
Q_UNUSED(playcount);
|
||||
|
||||
return Result::ErrorCode::Unsupported;
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagParser::SaveSongRatingToFile(TagParser::Tag *tag, const float rating) const {
|
||||
|
||||
tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(ConvertToPOPMRating(rating)));
|
||||
|
||||
}
|
||||
|
||||
TagReaderBase::Result TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const float rating) const {
|
||||
|
||||
if (filename.isEmpty()) return Result::ErrorCode::FilenameMissing;
|
||||
|
||||
qLog(Debug) << "Saving song rating to" << filename;
|
||||
|
||||
try {
|
||||
TagParser::MediaFileInfo taginfo;
|
||||
TagParser::Diagnostics diag;
|
||||
TagParser::AbortableProgressFeedback progress;
|
||||
#ifdef Q_OS_WIN32
|
||||
taginfo.setPath(filename.toStdWString().toStdString());
|
||||
#else
|
||||
taginfo.setPath(QFile::encodeName(filename).toStdString());
|
||||
#endif
|
||||
taginfo.open(false);
|
||||
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTracks(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return Result::ErrorCode::FileParseError;
|
||||
}
|
||||
|
||||
if (taginfo.tags().size() <= 0) {
|
||||
taginfo.createAppropriateTags();
|
||||
}
|
||||
|
||||
for (TagParser::Tag *tag : taginfo.tags()) {
|
||||
SaveSongRatingToFile(tag, rating);
|
||||
}
|
||||
|
||||
taginfo.applyChanges(diag, progress);
|
||||
taginfo.close();
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
qLog(Debug) << QString::fromStdString(msg.message());
|
||||
}
|
||||
|
||||
return Result::ErrorCode::Success;
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
return Result::ErrorCode::FileParseError;
|
||||
|
||||
}
|
||||
61
ext/libstrawberry-tagreader/tagreadertagparser.h
Normal file
61
ext/libstrawberry-tagreader/tagreadertagparser.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2021-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERTAGPARSER_H
|
||||
#define TAGREADERTAGPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <tagparser/tag.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
#include "tagreaderbase.h"
|
||||
|
||||
/*
|
||||
* This class holds all useful methods to read and write tags from/to files.
|
||||
* You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient)
|
||||
*/
|
||||
class TagReaderTagParser : public TagReaderBase {
|
||||
public:
|
||||
explicit TagReaderTagParser();
|
||||
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
Result ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
Result WriteFile(const QString &filename, const spb::tagreader::WriteFileRequest &request) const override;
|
||||
|
||||
Result LoadEmbeddedArt(const QString &filename, QByteArray &data) const override;
|
||||
Result SaveEmbeddedArt(const QString &filename, const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
|
||||
|
||||
Result SaveSongPlaycountToFile(const QString &filename, const uint playcount) const override;
|
||||
Result SaveSongRatingToFile(const QString &filename, const float rating) const override;
|
||||
|
||||
private:
|
||||
void SaveSongPlaycountToFile(TagParser::Tag *tag, const uint playcount) const;
|
||||
void SaveSongRatingToFile(TagParser::Tag *tag, const float rating) const;
|
||||
void SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const;
|
||||
|
||||
public:
|
||||
Q_DISABLE_COPY(TagReaderTagParser)
|
||||
};
|
||||
|
||||
#endif // TAGREADERTAGPARSER_H
|
||||
18
ext/macdeploycheck/CMakeLists.txt
Normal file
18
ext/macdeploycheck/CMakeLists.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
qt_wrap_cpp(MACDEPLOYCHECK_MOC ${CMAKE_SOURCE_DIR}/ext/libstrawberry-common/core/logging.h)
|
||||
|
||||
add_executable(macdeploycheck macdeploycheck.cpp ${CMAKE_SOURCE_DIR}/ext/libstrawberry-common/core/logging.cpp ${MACDEPLOYCHECK_MOC})
|
||||
target_include_directories(macdeploycheck PUBLIC SYSTEM
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
)
|
||||
target_include_directories(macdeploycheck PUBLIC
|
||||
${CMAKE_SOURCE_DIR}/ext/libstrawberry-common
|
||||
${CMAKE_BINARY_DIR}/src
|
||||
)
|
||||
target_link_directories(macdeploycheck PUBLIC ${GLIB_LIBRARY_DIRS})
|
||||
target_link_libraries(macdeploycheck PUBLIC
|
||||
"-framework AppKit"
|
||||
${GLIB_LIBRARIES}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
)
|
||||
147
ext/macdeploycheck/macdeploycheck.cpp
Normal file
147
ext/macdeploycheck/macdeploycheck.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2021, 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 <QCoreApplication>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
int main(int argc, char **argv);
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
logging::Init();
|
||||
|
||||
qLog(Info) << "Running macdeploycheck";
|
||||
|
||||
if (argc < 1) {
|
||||
qLog(Error) << "Usage: macdeploycheck <bundledir>";
|
||||
return 1;
|
||||
}
|
||||
QString bundle_path = QString::fromLocal8Bit(argv[1]);
|
||||
|
||||
bool success = true;
|
||||
|
||||
QDirIterator iter(bundle_path, QDir::Files | QDir::NoSymLinks, QDirIterator::Subdirectories);
|
||||
while (iter.hasNext()) {
|
||||
|
||||
iter.next();
|
||||
|
||||
QString filepath = iter.fileInfo().filePath();
|
||||
|
||||
// Ignore these files.
|
||||
if (filepath.endsWith(".plist") ||
|
||||
filepath.endsWith(".icns") ||
|
||||
filepath.endsWith(".prl") ||
|
||||
filepath.endsWith(".conf") ||
|
||||
filepath.endsWith(".h") ||
|
||||
filepath.endsWith(".nib") ||
|
||||
filepath.endsWith(".strings") ||
|
||||
filepath.endsWith(".css") ||
|
||||
filepath.endsWith("CodeResources") ||
|
||||
filepath.endsWith("PkgInfo") ||
|
||||
filepath.endsWith(".modulemap")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QProcess otool;
|
||||
otool.start("otool", QStringList() << "-L" << filepath);
|
||||
otool.waitForFinished();
|
||||
if (otool.exitStatus() != QProcess::NormalExit || otool.exitCode() != 0) {
|
||||
qLog(Error) << "otool failed for" << filepath << ":" << otool.readAllStandardError();
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
QString output = otool.readAllStandardOutput();
|
||||
QStringList output_lines = output.split("\n", Qt::SkipEmptyParts);
|
||||
if (output_lines.size() < 2) {
|
||||
qLog(Error) << "Could not parse otool output:" << output;
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
QString first_line = output_lines.first();
|
||||
if (first_line.endsWith(':')) first_line.chop(1);
|
||||
if (first_line == filepath) {
|
||||
output_lines.removeFirst();
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "First line" << first_line << "does not match" << filepath;
|
||||
success = false;
|
||||
}
|
||||
QRegularExpression regexp(QStringLiteral("^\\t(.+) \\(compatibility version (\\d+\\.\\d+\\.\\d+), current version (\\d+\\.\\d+\\.\\d+)(, weak|, reexport)?\\)$"));
|
||||
for (const QString &output_line : output_lines) {
|
||||
|
||||
//qDebug() << "Final check on" << filepath << output_line;
|
||||
|
||||
QRegularExpressionMatch match = regexp.match(output_line);
|
||||
if (match.hasMatch()) {
|
||||
QString library = match.captured(1);
|
||||
if (QFileInfo(library).fileName() == QFileInfo(filepath).fileName()) { // It's this.
|
||||
continue;
|
||||
}
|
||||
else if (library.startsWith("@executable_path")) {
|
||||
QString real_path = library;
|
||||
real_path = real_path.replace("@executable_path", bundle_path + "/Contents/MacOS");
|
||||
if (!QFile::exists(real_path)) {
|
||||
qLog(Error) << real_path << "does not exist for" << filepath;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else if (library.startsWith("@rpath")) {
|
||||
QString real_path = library;
|
||||
real_path = real_path.replace("@rpath", bundle_path + "/Contents/Frameworks");
|
||||
if (!QFile::exists(real_path) && !real_path.endsWith("QtSvg")) { // FIXME: Ignore broken svg image plugin.
|
||||
qLog(Error) << real_path << "does not exist for" << filepath;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else if (library.startsWith("@loader_path")) {
|
||||
QString loader_path = QFileInfo(filepath).path();
|
||||
QString real_path = library;
|
||||
real_path = real_path.replace("@loader_path", loader_path);
|
||||
if (!QFile::exists(real_path)) {
|
||||
qLog(Error) << real_path << "does not exist for" << filepath;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else if (library.startsWith("/System/Library/") || library.startsWith("/usr/lib/")) { // System library
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "File" << filepath << "points to" << library;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Could not parse otool output line:" << output_line;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success ? 0 : 1;
|
||||
|
||||
}
|
||||
58
ext/strawberry-tagreader/CMakeLists.txt
Normal file
58
ext/strawberry-tagreader/CMakeLists.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
|
||||
|
||||
set(SOURCES main.cpp tagreaderworker.cpp)
|
||||
set(HEADERS tagreaderworker.h)
|
||||
|
||||
qt_wrap_cpp(MOC ${HEADERS})
|
||||
|
||||
add_executable(strawberry-tagreader ${SOURCES} ${MOC} ${QRC})
|
||||
|
||||
target_include_directories(strawberry-tagreader SYSTEM PRIVATE
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
${PROTOBUF_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_include_directories(strawberry-tagreader PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/ext/libstrawberry-common
|
||||
${CMAKE_SOURCE_DIR}/ext/libstrawberry-tagreader
|
||||
${CMAKE_BINARY_DIR}/ext/libstrawberry-tagreader
|
||||
${CMAKE_BINARY_DIR}/src
|
||||
)
|
||||
|
||||
target_link_directories(strawberry-tagreader PRIVATE ${GLIB_LIBRARY_DIRS})
|
||||
|
||||
target_link_libraries(strawberry-tagreader PRIVATE
|
||||
${GLIB_LIBRARIES}
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::Network
|
||||
libstrawberry-common
|
||||
libstrawberry-tagreader
|
||||
)
|
||||
|
||||
if(HAVE_TAGLIB)
|
||||
target_include_directories(strawberry-tagreader SYSTEM PRIVATE ${TAGLIB_INCLUDE_DIRS})
|
||||
target_link_directories(strawberry-tagreader PRIVATE ${TAGLIB_LIBRARY_DIRS})
|
||||
target_link_libraries(strawberry-tagreader PRIVATE ${TAGLIB_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(HAVE_TAGPARSER)
|
||||
target_include_directories(strawberry-tagreader SYSTEM PRIVATE ${TAGPARSER_INCLUDE_DIRS})
|
||||
target_link_directories(strawberry-tagreader PRIVATE ${TAGPARSER_LIBRARY_DIRS})
|
||||
target_link_libraries(strawberry-tagreader PRIVATE ${TAGPARSER_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(FREEBSD)
|
||||
target_link_libraries(strawberry-tagreader PRIVATE execinfo)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(strawberry-tagreader PRIVATE /System/Library/Frameworks/Foundation.framework)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
install(TARGETS strawberry-tagreader DESTINATION ${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns)
|
||||
else()
|
||||
install(TARGETS strawberry-tagreader RUNTIME DESTINATION bin)
|
||||
endif()
|
||||
61
ext/strawberry-tagreader/main.cpp
Normal file
61
ext/strawberry-tagreader/main.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2021, 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 "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QLocalSocket>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "tagreaderworker.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
QCoreApplication a(argc, argv);
|
||||
QStringList args(a.arguments());
|
||||
|
||||
if (args.count() != 2) {
|
||||
std::cerr << "This program is used internally by Strawberry to parse tags in music files\n"
|
||||
"without exposing the whole application to crashes caused by malformed\n"
|
||||
"files. It is not meant to be run on its own.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
logging::Init();
|
||||
qLog(Info) << "TagReader worker connecting to" << args[1];
|
||||
|
||||
// Connect to the parent process.
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(args[1]);
|
||||
if (!socket.waitForConnected(2000)) {
|
||||
std::cerr << "Failed to connect to the parent process.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
TagReaderWorker worker(&socket);
|
||||
|
||||
return a.exec();
|
||||
|
||||
}
|
||||
194
ext/strawberry-tagreader/tagreaderworker.cpp
Normal file
194
ext/strawberry-tagreader/tagreaderworker.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <utility>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QObject>
|
||||
#include <QIODevice>
|
||||
#include <QByteArray>
|
||||
|
||||
#include "tagreaderworker.h"
|
||||
|
||||
#ifdef HAVE_TAGLIB
|
||||
# include "tagreadertaglib.h"
|
||||
# include "tagreadergme.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_TAGPARSER
|
||||
# include "tagreadertagparser.h"
|
||||
#endif
|
||||
|
||||
using std::make_shared;
|
||||
using std::shared_ptr;
|
||||
|
||||
TagReaderWorker::TagReaderWorker(QIODevice *socket, QObject *parent)
|
||||
: AbstractMessageHandler<spb::tagreader::Message>(socket, parent) {
|
||||
|
||||
#ifdef HAVE_TAGLIB
|
||||
tagreaders_ << make_shared<TagReaderTagLib>();
|
||||
tagreaders_ << make_shared<TagReaderGME>();
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_TAGPARSER
|
||||
tagreaders_ << make_shared<TagReaderTagParser>();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void TagReaderWorker::MessageArrived(const spb::tagreader::Message &message) {
|
||||
|
||||
spb::tagreader::Message reply;
|
||||
|
||||
HandleMessage(message, reply);
|
||||
SendReply(message, &reply);
|
||||
|
||||
}
|
||||
|
||||
void TagReaderWorker::DeviceClosed() {
|
||||
|
||||
AbstractMessageHandler<spb::tagreader::Message>::DeviceClosed();
|
||||
|
||||
QCoreApplication::exit();
|
||||
|
||||
}
|
||||
|
||||
void TagReaderWorker::HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply) {
|
||||
|
||||
for (shared_ptr<TagReaderBase> reader : std::as_const(tagreaders_)) {
|
||||
|
||||
if (message.has_is_media_file_request()) {
|
||||
const QString filename = QString::fromStdString(message.is_media_file_request().filename());
|
||||
const bool success = reader->IsMediaFile(filename);
|
||||
reply.mutable_is_media_file_response()->set_success(success);
|
||||
if (success) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (message.has_read_file_request()) {
|
||||
const QString filename = QString::fromStdString(message.read_file_request().filename());
|
||||
spb::tagreader::ReadFileResponse *response = reply.mutable_read_file_response();
|
||||
const TagReaderBase::Result result = reader->ReadFile(filename, response->mutable_metadata());
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.has_write_file_request()) {
|
||||
const QString filename = QString::fromStdString(message.write_file_request().filename());
|
||||
const TagReaderBase::Result result = reader->WriteFile(filename, message.write_file_request());
|
||||
spb::tagreader::WriteFileResponse *response = reply.mutable_write_file_response();
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.has_load_embedded_art_request()) {
|
||||
const QString filename = QString::fromStdString(message.load_embedded_art_request().filename());
|
||||
QByteArray data;
|
||||
const TagReaderBase::Result result = reader->LoadEmbeddedArt(filename, data);
|
||||
spb::tagreader::LoadEmbeddedArtResponse *response = reply.mutable_load_embedded_art_response();
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
response->set_data(data.toStdString());
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.has_save_embedded_art_request()) {
|
||||
const QString filename = QString::fromStdString(message.save_embedded_art_request().filename());
|
||||
const TagReaderBase::Result result = reader->SaveEmbeddedArt(filename, message.save_embedded_art_request());
|
||||
spb::tagreader::SaveEmbeddedArtResponse *response = reply.mutable_save_embedded_art_response();
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.has_save_song_playcount_to_file_request()) {
|
||||
const QString filename = QString::fromStdString(message.save_song_playcount_to_file_request().filename());
|
||||
const TagReaderBase::Result result = reader->SaveSongPlaycountToFile(filename, message.save_song_playcount_to_file_request().playcount());
|
||||
spb::tagreader::SaveSongPlaycountToFileResponse *response = reply.mutable_save_song_playcount_to_file_response();
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.has_save_song_rating_to_file_request()) {
|
||||
const QString filename = QString::fromStdString(message.save_song_rating_to_file_request().filename());
|
||||
const TagReaderBase::Result result = reader->SaveSongRatingToFile(filename, message.save_song_rating_to_file_request().rating());
|
||||
spb::tagreader::SaveSongRatingToFileResponse *response = reply.mutable_save_song_rating_to_file_response();
|
||||
response->set_success(result.success());
|
||||
if (result.success()) {
|
||||
if (response->has_error()) {
|
||||
response->clear_error();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!response->has_error()) {
|
||||
response->set_error(TagReaderBase::ErrorString(result).toStdString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
54
ext/strawberry-tagreader/tagreaderworker.h
Normal file
54
ext/strawberry-tagreader/tagreaderworker.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERWORKER_H
|
||||
#define TAGREADERWORKER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
|
||||
#include "core/messagehandler.h"
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
class QIODevice;
|
||||
class TagReaderBase;
|
||||
|
||||
using std::shared_ptr;
|
||||
|
||||
class TagReaderWorker : public AbstractMessageHandler<spb::tagreader::Message> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TagReaderWorker(QIODevice *socket, QObject *parent = nullptr);
|
||||
|
||||
protected:
|
||||
void MessageArrived(const spb::tagreader::Message &message) override;
|
||||
void DeviceClosed() override;
|
||||
|
||||
private:
|
||||
void HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply);
|
||||
|
||||
QList<shared_ptr<TagReaderBase>> tagreaders_;
|
||||
};
|
||||
|
||||
#endif // TAGREADERWORKER_H
|
||||
1289
src/CMakeLists.txt
1289
src/CMakeLists.txt
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include <QWidget>
|
||||
#include <QList>
|
||||
#include <QVector>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QBasicTimer>
|
||||
@@ -50,9 +50,9 @@
|
||||
// Make an INSTRUCTIONS file
|
||||
// can't mod scope in analyze you have to use transform for 2D use setErasePixmap Qt function insetead of m_background
|
||||
|
||||
AnalyzerBase::AnalyzerBase(QWidget *parent, const uint scope_size)
|
||||
AnalyzerBase::AnalyzerBase(QWidget *parent, const uint scopeSize)
|
||||
: QWidget(parent),
|
||||
fht_(new FHT(scope_size)),
|
||||
fht_(new FHT(scopeSize)),
|
||||
engine_(nullptr),
|
||||
lastscope_(512),
|
||||
new_frame_(false),
|
||||
@@ -67,13 +67,11 @@ AnalyzerBase::~AnalyzerBase() {
|
||||
delete fht_;
|
||||
}
|
||||
|
||||
void AnalyzerBase::showEvent(QShowEvent *e) {
|
||||
Q_UNUSED(e)
|
||||
void AnalyzerBase::showEvent(QShowEvent*) {
|
||||
timer_.start(timeout(), this);
|
||||
}
|
||||
|
||||
void AnalyzerBase::hideEvent(QHideEvent *e) {
|
||||
Q_UNUSED(e)
|
||||
void AnalyzerBase::hideEvent(QHideEvent*) {
|
||||
timer_.stop();
|
||||
}
|
||||
|
||||
@@ -89,7 +87,7 @@ void AnalyzerBase::ChangeTimeout(const int timeout) {
|
||||
|
||||
void AnalyzerBase::transform(Scope &scope) {
|
||||
|
||||
QList<float> aux(fht_->size());
|
||||
QVector<float> aux(fht_->size());
|
||||
if (static_cast<quint64>(aux.size()) >= scope.size()) {
|
||||
std::copy(scope.begin(), scope.end(), aux.begin());
|
||||
}
|
||||
@@ -211,28 +209,28 @@ void AnalyzerBase::demo(QPainter &p) {
|
||||
|
||||
}
|
||||
|
||||
void AnalyzerBase::interpolate(const Scope &in_scope, Scope &out_scope) {
|
||||
void AnalyzerBase::interpolate(const Scope &inVec, Scope &outVec) {
|
||||
|
||||
double pos = 0.0;
|
||||
const double step = static_cast<double>(in_scope.size()) / static_cast<double>(out_scope.size());
|
||||
const double step = static_cast<double>(inVec.size()) / static_cast<double>(outVec.size());
|
||||
|
||||
for (uint i = 0; i < out_scope.size(); ++i, pos += step) {
|
||||
for (uint i = 0; i < outVec.size(); ++i, pos += step) {
|
||||
const double error = pos - std::floor(pos);
|
||||
const uint64_t offset = static_cast<uint64_t>(pos);
|
||||
|
||||
uint64_t indexLeft = offset + 0;
|
||||
|
||||
if (indexLeft >= in_scope.size()) {
|
||||
indexLeft = in_scope.size() - 1;
|
||||
if (indexLeft >= inVec.size()) {
|
||||
indexLeft = inVec.size() - 1;
|
||||
}
|
||||
|
||||
uint64_t indexRight = offset + 1;
|
||||
|
||||
if (indexRight >= in_scope.size()) {
|
||||
indexRight = in_scope.size() - 1;
|
||||
if (indexRight >= inVec.size()) {
|
||||
indexRight = inVec.size() - 1;
|
||||
}
|
||||
|
||||
out_scope[i] = in_scope[indexLeft] * (1.0F - static_cast<float>(error)) + in_scope[indexRight] * static_cast<float>(error);
|
||||
outVec[i] = inVec[indexLeft] * (1.0F - static_cast<float>(error)) + inVec[indexRight] * static_cast<float>(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,12 +31,14 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QBasicTimer>
|
||||
#include <QString>
|
||||
#include <QPainter>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "analyzer/fht.h"
|
||||
#include "engine/enginebase.h"
|
||||
|
||||
@@ -61,22 +63,22 @@ class AnalyzerBase : public QWidget {
|
||||
|
||||
protected:
|
||||
using Scope = std::vector<float>;
|
||||
explicit AnalyzerBase(QWidget *parent, const uint scope_size = 7);
|
||||
explicit AnalyzerBase(QWidget*, const uint scopeSize = 7);
|
||||
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
void showEvent(QShowEvent *e) override;
|
||||
void hideEvent(QHideEvent*) override;
|
||||
void showEvent(QShowEvent*) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void timerEvent(QTimerEvent *e) override;
|
||||
|
||||
int resizeExponent(int exp);
|
||||
int resizeForBands(const int bands);
|
||||
int resizeExponent(int);
|
||||
int resizeForBands(const int);
|
||||
virtual void init() {}
|
||||
virtual void transform(Scope &scope);
|
||||
virtual void analyze(QPainter &p, const Scope &s, const bool new_frame) = 0;
|
||||
virtual void transform(Scope&);
|
||||
virtual void analyze(QPainter &p, const Scope&, const bool new_frame) = 0;
|
||||
virtual void demo(QPainter &p);
|
||||
|
||||
void interpolate(const Scope &in_scope, Scope &out_scope);
|
||||
void initSin(Scope &v, const uint size = 6000);
|
||||
void interpolate(const Scope&, Scope&);
|
||||
void initSin(Scope&, const uint = 6000);
|
||||
|
||||
protected:
|
||||
QBasicTimer timer_;
|
||||
|
||||
@@ -44,13 +44,12 @@
|
||||
#include "waverubberanalyzer.h"
|
||||
#include "rainbowanalyzer.h"
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/settings.h"
|
||||
#include "engine/enginebase.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
const char *AnalyzerContainer::kSettingsGroup = "Analyzer";
|
||||
const char *AnalyzerContainer::kSettingsFramerate = "framerate";
|
||||
@@ -112,8 +111,16 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
|
||||
|
||||
void AnalyzerContainer::mouseReleaseEvent(QMouseEvent *e) {
|
||||
|
||||
if (engine_->type() != EngineBase::Type::GStreamer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e->button() == Qt::RightButton) {
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
context_menu_->popup(e->globalPosition().toPoint());
|
||||
#else
|
||||
context_menu_->popup(e->globalPos());
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
@@ -134,12 +141,10 @@ void AnalyzerContainer::SetEngine(SharedPtr<EngineBase> engine) {
|
||||
}
|
||||
|
||||
void AnalyzerContainer::DisableAnalyzer() {
|
||||
|
||||
delete current_analyzer_;
|
||||
current_analyzer_ = nullptr;
|
||||
|
||||
Save();
|
||||
|
||||
}
|
||||
|
||||
void AnalyzerContainer::ChangeAnalyzer(const int id) {
|
||||
@@ -182,7 +187,7 @@ void AnalyzerContainer::Load() {
|
||||
|
||||
Settings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
QString type = s.value("type", u"BlockAnalyzer"_s).toString();
|
||||
QString type = s.value("type", QStringLiteral("BlockAnalyzer")).toString();
|
||||
current_framerate_ = s.value(kSettingsFramerate, kMediumFramerate).toInt();
|
||||
s.endGroup();
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
#include <QAction>
|
||||
#include <QActionGroup>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "engine/enginebase.h"
|
||||
|
||||
class QTimer;
|
||||
|
||||
@@ -340,9 +340,7 @@ QColor ensureContrast(const QColor &bg, const QColor &fg, int amount) {
|
||||
|
||||
}
|
||||
|
||||
void BlockAnalyzer::paletteChange(const QPalette &_palette) {
|
||||
|
||||
Q_UNUSED(_palette)
|
||||
void BlockAnalyzer::paletteChange(const QPalette&) {
|
||||
|
||||
const QColor bg = palette().color(QPalette::Window);
|
||||
const QColor fg = ensureContrast(bg, palette().color(QPalette::Highlight));
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVector>
|
||||
#include <QString>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
@@ -49,7 +49,7 @@ class BlockAnalyzer : public AnalyzerBase {
|
||||
void transform(Scope&) override;
|
||||
void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
|
||||
void resizeEvent(QResizeEvent*) override;
|
||||
virtual void paletteChange(const QPalette &_palette);
|
||||
virtual void paletteChange(const QPalette&);
|
||||
void framerateChanged() override;
|
||||
|
||||
void drawBackground();
|
||||
@@ -65,12 +65,12 @@ class BlockAnalyzer : public AnalyzerBase {
|
||||
QPixmap background_;
|
||||
QPixmap canvas_;
|
||||
Scope scope_; // so we don't create a vector every frame
|
||||
QList<double> store_; // current bar heights
|
||||
QList<double> yscale_;
|
||||
QVector<double> store_; // current bar heights
|
||||
QVector<double> yscale_;
|
||||
|
||||
QList<QPixmap> fade_bars_;
|
||||
QList<int> fade_pos_;
|
||||
QList<int> fade_intensity_;
|
||||
QVector<QPixmap> fade_bars_;
|
||||
QVector<int> fade_pos_;
|
||||
QVector<int> fade_intensity_;
|
||||
|
||||
double step_; // rows to fall per frame
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <QList>
|
||||
#include <QVector>
|
||||
#include <QtMath>
|
||||
|
||||
FHT::FHT(uint n) : num_((n < 3) ? 0 : 1 << n), exp2_((n < 3) ? -1 : static_cast<int>(n)) {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#ifndef FHT_H
|
||||
#define FHT_H
|
||||
|
||||
#include <QList>
|
||||
#include <QVector>
|
||||
|
||||
/**
|
||||
* Implementation of the Hartley Transform after Bracewell's discrete
|
||||
@@ -37,9 +37,9 @@ class FHT {
|
||||
const int num_;
|
||||
const int exp2_;
|
||||
|
||||
QList<float> buf_vector_;
|
||||
QList<float> tab_vector_;
|
||||
QList<int> log_vector_;
|
||||
QVector<float> buf_vector_;
|
||||
QVector<float> tab_vector_;
|
||||
QVector<int> log_vector_;
|
||||
|
||||
float *buf_();
|
||||
float *tab_();
|
||||
|
||||
@@ -41,8 +41,6 @@
|
||||
#include "fht.h"
|
||||
#include "analyzerbase.h"
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
const char *NyanCatAnalyzer::kName = "Nyanalyzer Cat";
|
||||
const char *RainbowDashAnalyzer::kName = "Rainbow Dash";
|
||||
|
||||
@@ -70,8 +68,8 @@ RainbowAnalyzer::RainbowAnalyzer(const RainbowType rbtype, QWidget *parent)
|
||||
background_brush_(QColor(0x0f, 0x43, 0x73)) {
|
||||
|
||||
rainbowtype = rbtype;
|
||||
cat_dash_[0] = QPixmap(u":/pictures/nyancat.png"_s);
|
||||
cat_dash_[1] = QPixmap(u":/pictures/rainbowdash.png"_s);
|
||||
cat_dash_[0] = QPixmap(QStringLiteral(":/pictures/nyancat.png"));
|
||||
cat_dash_[1] = QPixmap(QStringLiteral(":/pictures/rainbowdash.png"));
|
||||
memset(history_, 0, sizeof(history_));
|
||||
|
||||
for (int i = 0; i < kRainbowBands; ++i) {
|
||||
|
||||
@@ -82,9 +82,9 @@ void WaveRubberAnalyzer::analyze(QPainter &p, const Scope &s, const bool new_fra
|
||||
|
||||
}
|
||||
|
||||
void WaveRubberAnalyzer::transform(Scope &scope) {
|
||||
void WaveRubberAnalyzer::transform(Scope &s) {
|
||||
// No need transformation for waveform analyzer
|
||||
Q_UNUSED(scope);
|
||||
Q_UNUSED(s);
|
||||
}
|
||||
|
||||
void WaveRubberAnalyzer::demo(QPainter &p) {
|
||||
|
||||
@@ -30,34 +30,31 @@
|
||||
#include <QSettings>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/database.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/thread.h"
|
||||
#include "core/song.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/settings.h"
|
||||
#include "tagreader/tagreaderclient.h"
|
||||
#include "utilities/threadutils.h"
|
||||
#include "collectionlibrary.h"
|
||||
#include "collection.h"
|
||||
#include "collectionwatcher.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "constants/collectionsettings.h"
|
||||
#include "scrobbler/lastfmimport.h"
|
||||
#include "settings/collectionsettingspage.h"
|
||||
|
||||
using std::make_shared;
|
||||
|
||||
const char *CollectionLibrary::kSongsTable = "songs";
|
||||
const char *CollectionLibrary::kDirsTable = "directories";
|
||||
const char *CollectionLibrary::kSubdirsTable = "subdirectories";
|
||||
const char *SCollection::kSongsTable = "songs";
|
||||
const char *SCollection::kDirsTable = "directories";
|
||||
const char *SCollection::kSubdirsTable = "subdirectories";
|
||||
|
||||
CollectionLibrary::CollectionLibrary(const SharedPtr<Database> database,
|
||||
const SharedPtr<TaskManager> task_manager,
|
||||
const SharedPtr<TagReaderClient> tagreader_client,
|
||||
const SharedPtr<AlbumCoverLoader> albumcover_loader,
|
||||
QObject *parent)
|
||||
SCollection::SCollection(Application *app, QObject *parent)
|
||||
: QObject(parent),
|
||||
task_manager_(task_manager),
|
||||
tagreader_client_(tagreader_client),
|
||||
app_(app),
|
||||
backend_(nullptr),
|
||||
model_(nullptr),
|
||||
watcher_(nullptr),
|
||||
@@ -66,23 +63,23 @@ CollectionLibrary::CollectionLibrary(const SharedPtr<Database> database,
|
||||
save_playcounts_to_files_(false),
|
||||
save_ratings_to_files_(false) {
|
||||
|
||||
setObjectName(QLatin1String(QObject::metaObject()->className()));
|
||||
setObjectName(QLatin1String(metaObject()->className()));
|
||||
|
||||
original_thread_ = thread();
|
||||
|
||||
backend_ = make_shared<CollectionBackend>();
|
||||
backend()->moveToThread(database->thread());
|
||||
qLog(Debug) << &*backend_ << "moved to thread" << database->thread();
|
||||
backend()->moveToThread(app->database()->thread());
|
||||
qLog(Debug) << &*backend_ << "moved to thread" << app->database()->thread();
|
||||
|
||||
backend_->Init(database, task_manager, Song::Source::Collection, QLatin1String(kSongsTable), QLatin1String(kDirsTable), QLatin1String(kSubdirsTable));
|
||||
backend_->Init(app->database(), app->task_manager(), Song::Source::Collection, QLatin1String(kSongsTable), QLatin1String(kDirsTable), QLatin1String(kSubdirsTable));
|
||||
|
||||
model_ = new CollectionModel(backend_, albumcover_loader, this);
|
||||
model_ = new CollectionModel(backend_, app_, this);
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
CollectionLibrary::~CollectionLibrary() {
|
||||
SCollection::~SCollection() {
|
||||
|
||||
if (watcher_) {
|
||||
watcher_->Abort();
|
||||
@@ -95,9 +92,9 @@ CollectionLibrary::~CollectionLibrary() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::Init() {
|
||||
void SCollection::Init() {
|
||||
|
||||
watcher_ = new CollectionWatcher(Song::Source::Collection, task_manager_, tagreader_client_, backend_);
|
||||
watcher_ = new CollectionWatcher(Song::Source::Collection);
|
||||
watcher_thread_ = new Thread(this);
|
||||
watcher_thread_->setObjectName(watcher_->objectName());
|
||||
|
||||
@@ -109,11 +106,14 @@ void CollectionLibrary::Init() {
|
||||
|
||||
watcher_thread_->start(QThread::IdlePriority);
|
||||
|
||||
QObject::connect(&*backend_, &CollectionBackend::Error, this, &CollectionLibrary::Error);
|
||||
watcher_->set_backend(backend_);
|
||||
watcher_->set_task_manager(app_->task_manager());
|
||||
|
||||
QObject::connect(&*backend_, &CollectionBackend::Error, this, &SCollection::Error);
|
||||
QObject::connect(&*backend_, &CollectionBackend::DirectoryAdded, watcher_, &CollectionWatcher::AddDirectory);
|
||||
QObject::connect(&*backend_, &CollectionBackend::DirectoryDeleted, watcher_, &CollectionWatcher::RemoveDirectory);
|
||||
QObject::connect(&*backend_, &CollectionBackend::SongsRatingChanged, this, &CollectionLibrary::SongsRatingChanged);
|
||||
QObject::connect(&*backend_, &CollectionBackend::SongsStatisticsChanged, this, &CollectionLibrary::SongsPlaycountChanged);
|
||||
QObject::connect(&*backend_, &CollectionBackend::SongsRatingChanged, this, &SCollection::SongsRatingChanged);
|
||||
QObject::connect(&*backend_, &CollectionBackend::SongsStatisticsChanged, this, &SCollection::SongsPlaycountChanged);
|
||||
|
||||
QObject::connect(watcher_, &CollectionWatcher::NewOrUpdatedSongs, &*backend_, &CollectionBackend::AddOrUpdateSongs);
|
||||
QObject::connect(watcher_, &CollectionWatcher::SongsMTimeUpdated, &*backend_, &CollectionBackend::UpdateMTimesOnly);
|
||||
@@ -125,27 +125,30 @@ void CollectionLibrary::Init() {
|
||||
QObject::connect(watcher_, &CollectionWatcher::CompilationsNeedUpdating, &*backend_, &CollectionBackend::CompilationsNeedUpdating);
|
||||
QObject::connect(watcher_, &CollectionWatcher::UpdateLastSeen, &*backend_, &CollectionBackend::UpdateLastSeen);
|
||||
|
||||
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdateLastPlayed, &*backend_, &CollectionBackend::UpdateLastPlayed);
|
||||
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdatePlayCount, &*backend_, &CollectionBackend::UpdatePlayCount);
|
||||
|
||||
// This will start the watcher checking for updates
|
||||
backend_->LoadDirectoriesAsync();
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::Exit() {
|
||||
void SCollection::Exit() {
|
||||
|
||||
wait_for_exit_ << &*backend_ << watcher_;
|
||||
|
||||
QObject::disconnect(&*backend_, nullptr, watcher_, nullptr);
|
||||
QObject::disconnect(watcher_, nullptr, &*backend_, nullptr);
|
||||
|
||||
QObject::connect(&*backend_, &CollectionBackend::ExitFinished, this, &CollectionLibrary::ExitReceived);
|
||||
QObject::connect(watcher_, &CollectionWatcher::ExitFinished, this, &CollectionLibrary::ExitReceived);
|
||||
QObject::connect(&*backend_, &CollectionBackend::ExitFinished, this, &SCollection::ExitReceived);
|
||||
QObject::connect(watcher_, &CollectionWatcher::ExitFinished, this, &SCollection::ExitReceived);
|
||||
backend_->ExitAsync();
|
||||
watcher_->Abort();
|
||||
watcher_->ExitAsync();
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::ExitReceived() {
|
||||
void SCollection::ExitReceived() {
|
||||
|
||||
QObject *obj = sender();
|
||||
QObject::disconnect(obj, nullptr, this, nullptr);
|
||||
@@ -155,13 +158,13 @@ void CollectionLibrary::ExitReceived() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::IncrementalScan() { watcher_->IncrementalScanAsync(); }
|
||||
void SCollection::IncrementalScan() { watcher_->IncrementalScanAsync(); }
|
||||
|
||||
void CollectionLibrary::FullScan() { watcher_->FullScanAsync(); }
|
||||
void SCollection::FullScan() { watcher_->FullScanAsync(); }
|
||||
|
||||
void CollectionLibrary::StopScan() { watcher_->Stop(); }
|
||||
void SCollection::StopScan() { watcher_->Stop(); }
|
||||
|
||||
void CollectionLibrary::Rescan(const SongList &songs) {
|
||||
void SCollection::Rescan(const SongList &songs) {
|
||||
|
||||
qLog(Debug) << "Rescan" << songs.size() << "songs";
|
||||
if (!songs.isEmpty()) {
|
||||
@@ -170,58 +173,62 @@ void CollectionLibrary::Rescan(const SongList &songs) {
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::PauseWatcher() { watcher_->SetRescanPausedAsync(true); }
|
||||
void SCollection::PauseWatcher() { watcher_->SetRescanPausedAsync(true); }
|
||||
|
||||
void CollectionLibrary::ResumeWatcher() { watcher_->SetRescanPausedAsync(false); }
|
||||
void SCollection::ResumeWatcher() { watcher_->SetRescanPausedAsync(false); }
|
||||
|
||||
void CollectionLibrary::ReloadSettings() {
|
||||
void SCollection::ReloadSettings() {
|
||||
|
||||
watcher_->ReloadSettingsAsync();
|
||||
model_->ReloadSettings();
|
||||
|
||||
Settings s;
|
||||
s.beginGroup(CollectionSettings::kSettingsGroup);
|
||||
save_playcounts_to_files_ = s.value(CollectionSettings::kSavePlayCounts, false).toBool();
|
||||
save_ratings_to_files_ = s.value(CollectionSettings::kSaveRatings, false).toBool();
|
||||
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
|
||||
save_playcounts_to_files_ = s.value("save_playcounts", false).toBool();
|
||||
save_ratings_to_files_ = s.value("save_ratings", false).toBool();
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SyncPlaycountAndRatingToFilesAsync() {
|
||||
void SCollection::SyncPlaycountAndRatingToFilesAsync() {
|
||||
|
||||
(void)QtConcurrent::run(&CollectionLibrary::SyncPlaycountAndRatingToFiles, this);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
(void)QtConcurrent::run(&SCollection::SyncPlaycountAndRatingToFiles, this);
|
||||
#else
|
||||
(void)QtConcurrent::run(this, &SCollection::SyncPlaycountAndRatingToFiles);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SyncPlaycountAndRatingToFiles() {
|
||||
void SCollection::SyncPlaycountAndRatingToFiles() {
|
||||
|
||||
const int task_id = task_manager_->StartTask(tr("Saving playcounts and ratings"));
|
||||
task_manager_->SetTaskBlocksCollectionScans(task_id);
|
||||
const int task_id = app_->task_manager()->StartTask(tr("Saving playcounts and ratings"));
|
||||
app_->task_manager()->SetTaskBlocksCollectionScans(task_id);
|
||||
|
||||
const SongList songs = backend_->GetAllSongs();
|
||||
const qint64 nb_songs = songs.size();
|
||||
int i = 0;
|
||||
for (const Song &song : songs) {
|
||||
(void)tagreader_client_->SaveSongPlaycountBlocking(song.url().toLocalFile(), song.playcount());
|
||||
(void)tagreader_client_->SaveSongRatingBlocking(song.url().toLocalFile(), song.rating());
|
||||
task_manager_->SetTaskProgress(task_id, ++i, nb_songs);
|
||||
(void)TagReaderClient::Instance()->SaveSongPlaycountBlocking(song.url().toLocalFile(), song.playcount());
|
||||
(void)TagReaderClient::Instance()->SaveSongRatingBlocking(song.url().toLocalFile(), song.rating());
|
||||
app_->task_manager()->SetTaskProgress(task_id, ++i, nb_songs);
|
||||
}
|
||||
task_manager_->SetTaskFinished(task_id);
|
||||
app_->task_manager()->SetTaskFinished(task_id);
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SongsPlaycountChanged(const SongList &songs, const bool save_tags) const {
|
||||
void SCollection::SongsPlaycountChanged(const SongList &songs, const bool save_tags) {
|
||||
|
||||
if (save_tags || save_playcounts_to_files_) {
|
||||
tagreader_client_->SaveSongsPlaycountAsync(songs);
|
||||
app_->tag_reader_client()->SaveSongsPlaycount(songs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionLibrary::SongsRatingChanged(const SongList &songs, const bool save_tags) const {
|
||||
void SCollection::SongsRatingChanged(const SongList &songs, const bool save_tags) {
|
||||
|
||||
if (save_tags || save_ratings_to_files_) {
|
||||
tagreader_client_->SaveSongsRatingAsync(songs);
|
||||
app_->tag_reader_client()->SaveSongsRating(songs);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,30 +29,22 @@
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class QThread;
|
||||
class Application;
|
||||
class Thread;
|
||||
class Database;
|
||||
class TaskManager;
|
||||
class TagReaderClient;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
class CollectionWatcher;
|
||||
class AlbumCoverLoader;
|
||||
|
||||
class CollectionLibrary : public QObject {
|
||||
class SCollection : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CollectionLibrary(const SharedPtr<Database> database,
|
||||
const SharedPtr<TaskManager> task_manager,
|
||||
const SharedPtr<TagReaderClient> tagreader_client,
|
||||
const SharedPtr<AlbumCoverLoader> albumcover_loader,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
~CollectionLibrary() override;
|
||||
explicit SCollection(Application *app, QObject *parent = nullptr);
|
||||
~SCollection() override;
|
||||
|
||||
static const char *kSongsTable;
|
||||
static const char *kFtsTable;
|
||||
@@ -86,17 +78,15 @@ class CollectionLibrary : public QObject {
|
||||
|
||||
private Q_SLOTS:
|
||||
void ExitReceived();
|
||||
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false) const;
|
||||
void SongsRatingChanged(const SongList &songs, const bool save_tags = false) const;
|
||||
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false);
|
||||
void SongsRatingChanged(const SongList &songs, const bool save_tags = false);
|
||||
|
||||
Q_SIGNALS:
|
||||
void Error(const QString &error);
|
||||
void ExitFinished();
|
||||
|
||||
private:
|
||||
const SharedPtr<TaskManager> task_manager_;
|
||||
const SharedPtr<TagReaderClient> tagreader_client_;
|
||||
|
||||
Application *app_;
|
||||
SharedPtr<CollectionBackend> backend_;
|
||||
CollectionModel *model_;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
#include <QMutex>
|
||||
#include <QSet>
|
||||
#include <QMap>
|
||||
#include <QList>
|
||||
#include <QVector>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
@@ -45,11 +45,13 @@
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlError>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/database.h"
|
||||
#include "core/scopedtransaction.h"
|
||||
#include "core/song.h"
|
||||
#include "core/sqlrow.h"
|
||||
#include "smartplaylists/smartplaylistsearch.h"
|
||||
|
||||
#include "collectiondirectory.h"
|
||||
#include "collectionbackend.h"
|
||||
@@ -57,8 +59,6 @@
|
||||
#include "collectionquery.h"
|
||||
#include "collectiontask.h"
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
CollectionBackend::CollectionBackend(QObject *parent)
|
||||
: CollectionBackendInterface(parent),
|
||||
db_(nullptr),
|
||||
@@ -78,7 +78,7 @@ CollectionBackend::~CollectionBackend() {
|
||||
|
||||
void CollectionBackend::Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &dirs_table, const QString &subdirs_table) {
|
||||
|
||||
setObjectName(source == Song::Source::Collection ? QLatin1String(QObject::metaObject()->className()) : QStringLiteral("%1%2").arg(Song::DescriptionForSource(source), QLatin1String(QObject::metaObject()->className())));
|
||||
setObjectName(source == Song::Source::Collection ? QLatin1String(metaObject()->className()) : QStringLiteral("%1%2").arg(Song::DescriptionForSource(source), QLatin1String(metaObject()->className())));
|
||||
|
||||
db_ = db;
|
||||
task_manager_ = task_manager;
|
||||
@@ -208,8 +208,8 @@ void CollectionBackend::ChangeDirPath(const int id, const QString &old_path, con
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET path=:path WHERE ROWID=:id").arg(dirs_table_));
|
||||
q.BindValue(u":path"_s, new_path);
|
||||
q.BindValue(u":id"_s, id);
|
||||
q.BindValue(QStringLiteral(":path"), new_path);
|
||||
q.BindValue(QStringLiteral(":id"), id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -225,8 +225,8 @@ void CollectionBackend::ChangeDirPath(const int id, const QString &old_path, con
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET path=:path || substr(path, %2) WHERE directory=:id").arg(subdirs_table_).arg(path_len));
|
||||
q.BindValue(u":path"_s, new_url);
|
||||
q.BindValue(u":id"_s, id);
|
||||
q.BindValue(QStringLiteral(":path"), new_url);
|
||||
q.BindValue(QStringLiteral(":id"), id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -237,8 +237,8 @@ void CollectionBackend::ChangeDirPath(const int id, const QString &old_path, con
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET url=:path || substr(url, %2) WHERE directory=:id").arg(songs_table_).arg(path_len));
|
||||
q.BindValue(u":path"_s, new_url);
|
||||
q.BindValue(u":id"_s, id);
|
||||
q.BindValue(QStringLiteral(":path"), new_url);
|
||||
q.BindValue(QStringLiteral(":id"), id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -286,7 +286,7 @@ CollectionSubdirectoryList CollectionBackend::SubdirsInDirectory(const int id, Q
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT path, mtime FROM %1 WHERE directory_id = :dir").arg(subdirs_table_));
|
||||
q.BindValue(u":dir"_s, id);
|
||||
q.BindValue(QStringLiteral(":dir"), id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return CollectionSubdirectoryList();
|
||||
@@ -377,7 +377,7 @@ void CollectionBackend::AddDirectory(const QString &path) {
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE path = :path").arg(dirs_table_));
|
||||
q.BindValue(u":path"_s, path);
|
||||
q.BindValue(QStringLiteral(":path"), path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -389,7 +389,7 @@ void CollectionBackend::AddDirectory(const QString &path) {
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("INSERT INTO %1 (path, subdirs) VALUES (:path, 1)").arg(dirs_table_));
|
||||
q.BindValue(u":path"_s, path);
|
||||
q.BindValue(QStringLiteral(":path"), path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -421,7 +421,7 @@ void CollectionBackend::RemoveDirectory(const CollectionDirectory &dir) {
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, dir.id);
|
||||
q.BindValue(QStringLiteral(":id"), dir.id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -432,7 +432,7 @@ void CollectionBackend::RemoveDirectory(const CollectionDirectory &dir) {
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(dirs_table_));
|
||||
q.BindValue(u":id"_s, dir.id);
|
||||
q.BindValue(QStringLiteral(":id"), dir.id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -452,7 +452,7 @@ SongList CollectionBackend::FindSongsInDirectory(const int id) {
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE directory_id = :directory_id").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
q.BindValue(u":directory_id"_s, id);
|
||||
q.BindValue(QStringLiteral(":directory_id"), id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return SongList();
|
||||
@@ -475,7 +475,7 @@ SongList CollectionBackend::SongsWithMissingFingerprint(const int id) {
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE directory_id = :directory_id AND unavailable = 0 AND (fingerprint IS NULL OR fingerprint = '')").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
q.BindValue(u":directory_id"_s, id);
|
||||
q.BindValue(QStringLiteral(":directory_id"), id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return SongList();
|
||||
@@ -498,7 +498,7 @@ SongList CollectionBackend::SongsWithMissingLoudnessCharacteristics(const int id
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE directory_id = :directory_id AND unavailable = 0 AND (ebur128_integrated_loudness_lufs IS NULL OR ebur128_loudness_range_lu IS NULL)").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
q.BindValue(u":directory_id"_s, id);
|
||||
q.BindValue(QStringLiteral(":directory_id"), id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return SongList();
|
||||
@@ -541,8 +541,8 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
||||
// Delete the subdirectory
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
q.BindValue(QStringLiteral(":id"), subdir.directory_id);
|
||||
q.BindValue(QStringLiteral(":path"), subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -554,8 +554,8 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
q.BindValue(QStringLiteral(":id"), subdir.directory_id);
|
||||
q.BindValue(QStringLiteral(":path"), subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -566,9 +566,9 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
||||
if (exists) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE directory_id = :id AND path = :path").arg(subdirs_table_));
|
||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
q.BindValue(QStringLiteral(":mtime"), subdir.mtime);
|
||||
q.BindValue(QStringLiteral(":id"), subdir.directory_id);
|
||||
q.BindValue(QStringLiteral(":path"), subdir.path);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -577,9 +577,9 @@ void CollectionBackend::AddOrUpdateSubdirs(const CollectionSubdirectoryList &sub
|
||||
else {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("INSERT INTO %1 (directory_id, path, mtime) VALUES (:id, :path, :mtime)").arg(subdirs_table_));
|
||||
q.BindValue(u":id"_s, subdir.directory_id);
|
||||
q.BindValue(u":path"_s, subdir.path);
|
||||
q.BindValue(u":mtime"_s, subdir.mtime);
|
||||
q.BindValue(QStringLiteral(":id"), subdir.directory_id);
|
||||
q.BindValue(QStringLiteral(":path"), subdir.path);
|
||||
q.BindValue(QStringLiteral(":mtime"), subdir.mtime);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -635,7 +635,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
||||
if (!dirs_table_.isEmpty()) {
|
||||
SqlQuery check_dir(db);
|
||||
check_dir.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE ROWID = :id").arg(dirs_table_));
|
||||
check_dir.BindValue(u":id"_s, song.directory_id());
|
||||
check_dir.BindValue(QStringLiteral(":id"), song.directory_id());
|
||||
if (!check_dir.Exec()) {
|
||||
db_->ReportErrors(check_dir);
|
||||
return;
|
||||
@@ -656,7 +656,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(songs_table_, Song::kUpdateSpec));
|
||||
song.BindToQuery(&q);
|
||||
q.BindValue(u":id"_s, song.id());
|
||||
q.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -683,7 +683,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(songs_table_, Song::kUpdateSpec));
|
||||
new_song.BindToQuery(&q);
|
||||
q.BindValue(u":id"_s, new_song.id());
|
||||
q.BindValue(QStringLiteral(":id"), new_song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -768,7 +768,7 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(songs_table_, Song::kUpdateSpec));
|
||||
new_song.BindToQuery(&q);
|
||||
q.BindValue(u":id"_s, old_song.id());
|
||||
q.BindValue(QStringLiteral(":id"), old_song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -811,7 +811,7 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_));
|
||||
q.BindValue(u":id"_s, old_song.id());
|
||||
q.BindValue(QStringLiteral(":id"), old_song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -842,8 +842,8 @@ void CollectionBackend::UpdateMTimesOnly(const SongList &songs) {
|
||||
for (const Song &song : songs) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id").arg(songs_table_));
|
||||
q.BindValue(u":mtime"_s, song.mtime());
|
||||
q.BindValue(u":id"_s, song.id());
|
||||
q.BindValue(QStringLiteral(":mtime"), song.mtime());
|
||||
q.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -862,7 +862,7 @@ void CollectionBackend::DeleteSongs(const SongList &songs) {
|
||||
for (const Song &song : songs) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_));
|
||||
q.BindValue(u":id"_s, song.id());
|
||||
q.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -889,7 +889,7 @@ void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool u
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
for (const Song &song : songs) {
|
||||
query.BindValue(u":id"_s, song.id());
|
||||
query.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!query.Exec()) {
|
||||
db_->ReportErrors(query);
|
||||
return;
|
||||
@@ -916,7 +916,7 @@ QStringList CollectionBackend::GetAll(const QString &column, const CollectionFil
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionQuery query(db, songs_table_, filter_options);
|
||||
query.SetColumnSpec(u"DISTINCT "_s + column);
|
||||
query.SetColumnSpec(QStringLiteral("DISTINCT ") + column);
|
||||
query.AddCompilationRequirement(false);
|
||||
|
||||
if (!query.Exec()) {
|
||||
@@ -934,8 +934,7 @@ QStringList CollectionBackend::GetAll(const QString &column, const CollectionFil
|
||||
|
||||
QStringList CollectionBackend::GetAllArtists(const CollectionFilterOptions &opt) {
|
||||
|
||||
return GetAll(u"artist"_s, opt);
|
||||
|
||||
return GetAll(QStringLiteral("artist"), opt);
|
||||
}
|
||||
|
||||
QStringList CollectionBackend::GetAllArtistsWithAlbums(const CollectionFilterOptions &opt) {
|
||||
@@ -945,16 +944,16 @@ QStringList CollectionBackend::GetAllArtistsWithAlbums(const CollectionFilterOpt
|
||||
|
||||
// Albums with 'albumartist' field set:
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.SetColumnSpec(u"DISTINCT albumartist"_s);
|
||||
query.SetColumnSpec(QStringLiteral("DISTINCT albumartist"));
|
||||
query.AddCompilationRequirement(false);
|
||||
query.AddWhere(u"album"_s, ""_L1, u"!="_s);
|
||||
query.AddWhere(QStringLiteral("album"), QLatin1String(""), QStringLiteral("!="));
|
||||
|
||||
// Albums with no 'albumartist' (extract 'artist'):
|
||||
CollectionQuery query2(db, songs_table_, opt);
|
||||
query2.SetColumnSpec(u"DISTINCT artist"_s);
|
||||
query2.SetColumnSpec(QStringLiteral("DISTINCT artist"));
|
||||
query2.AddCompilationRequirement(false);
|
||||
query2.AddWhere(u"album"_s, ""_L1, u"!="_s);
|
||||
query2.AddWhere(u"albumartist"_s, ""_L1, u"="_s);
|
||||
query2.AddWhere(QStringLiteral("album"), QLatin1String(""), QStringLiteral("!="));
|
||||
query2.AddWhere(QStringLiteral("albumartist"), QLatin1String(""), QStringLiteral("="));
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
@@ -993,7 +992,7 @@ SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist,
|
||||
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.AddCompilationRequirement(false);
|
||||
query.AddWhere(u"effective_albumartist"_s, effective_albumartist);
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
|
||||
SongList songs;
|
||||
if (!ExecCollectionQuery(&query, songs)) {
|
||||
@@ -1011,8 +1010,8 @@ SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist,
|
||||
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.AddCompilationRequirement(false);
|
||||
query.AddWhere(u"effective_albumartist"_s, effective_albumartist);
|
||||
query.AddWhere(u"album"_s, album);
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
||||
SongList songs;
|
||||
if (!ExecCollectionQuery(&query, songs)) {
|
||||
@@ -1030,7 +1029,7 @@ SongList CollectionBackend::GetSongsByAlbum(const QString &album, const Collecti
|
||||
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.AddCompilationRequirement(false);
|
||||
query.AddWhere(u"album"_s, album);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
||||
SongList songs;
|
||||
if (!ExecCollectionQuery(&query, songs)) {
|
||||
@@ -1043,7 +1042,7 @@ SongList CollectionBackend::GetSongsByAlbum(const QString &album, const Collecti
|
||||
|
||||
bool CollectionBackend::ExecCollectionQuery(CollectionQuery *query, SongList &songs) {
|
||||
|
||||
query->SetColumnSpec(u"%songs_table.ROWID, "_s + Song::kColumnSpec);
|
||||
query->SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec);
|
||||
|
||||
if (!query->Exec()) return false;
|
||||
|
||||
@@ -1058,7 +1057,7 @@ bool CollectionBackend::ExecCollectionQuery(CollectionQuery *query, SongList &so
|
||||
|
||||
bool CollectionBackend::ExecCollectionQuery(CollectionQuery *query, SongMap &songs) {
|
||||
|
||||
query->SetColumnSpec(u"%songs_table.ROWID, "_s + Song::kColumnSpec);
|
||||
query->SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec);
|
||||
|
||||
if (!query->Exec()) return false;
|
||||
|
||||
@@ -1108,7 +1107,7 @@ SongList CollectionBackend::GetSongsByForeignId(const QStringList &ids, const QS
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QString in = ids.join(u',');
|
||||
QString in = ids.join(QLatin1Char(','));
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %3.ROWID, %2, %3.%4 FROM %3, %1 WHERE %3.%4 IN (in) AND %1.ROWID = %3.ROWID AND unavailable = 0").arg(songs_table_, Song::kColumnSpec, table, column, in));
|
||||
@@ -1117,7 +1116,7 @@ SongList CollectionBackend::GetSongsByForeignId(const QStringList &ids, const QS
|
||||
return SongList();
|
||||
}
|
||||
|
||||
QList<Song> ret(ids.count());
|
||||
QVector<Song> ret(ids.count());
|
||||
while (q.next()) {
|
||||
const QString foreign_id = q.value(static_cast<int>(Song::kColumns.count()) + 1).toString();
|
||||
const qint64 index = ids.indexOf(foreign_id);
|
||||
@@ -1139,7 +1138,7 @@ Song CollectionBackend::GetSongById(const int id, QSqlDatabase &db) {
|
||||
|
||||
SongList CollectionBackend::GetSongsById(const QStringList &ids, QSqlDatabase &db) {
|
||||
|
||||
QString in = ids.join(u',');
|
||||
QString in = ids.join(QLatin1Char(','));
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE ROWID IN (%3)").arg(Song::kRowIdColumnSpec, songs_table_, in));
|
||||
@@ -1165,11 +1164,11 @@ Song CollectionBackend::GetSongByUrl(const QUrl &url, const qint64 beginning) {
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND beginning = :beginning AND unavailable = 0").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
q.BindValue(u":url1"_s, url.toString());
|
||||
q.BindValue(u":url2"_s, url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(u":url3"_s, url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(u":url4"_s, url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(u":beginning"_s, beginning);
|
||||
q.BindValue(QStringLiteral(":url1"), url.toString());
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":beginning"), beginning);
|
||||
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
@@ -1194,11 +1193,11 @@ Song CollectionBackend::GetSongByUrlAndTrack(const QUrl &url, const int track) {
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND track = :track AND unavailable = 0").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
q.BindValue(u":url1"_s, url.toString());
|
||||
q.BindValue(u":url2"_s, url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(u":url3"_s, url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(u":url4"_s, url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(u":track"_s, track);
|
||||
q.BindValue(QStringLiteral(":url1"), url.toString());
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":track"), track);
|
||||
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
@@ -1223,11 +1222,11 @@ SongList CollectionBackend::GetSongsByUrl(const QUrl &url, const bool unavailabl
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = :unavailable").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
q.BindValue(u":url1"_s, url.toString());
|
||||
q.BindValue(u":url2"_s, url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(u":url3"_s, url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(u":url4"_s, url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(u":unavailable"_s, (unavailable ? 1 : 0));
|
||||
q.BindValue(QStringLiteral(":url1"), url.toString());
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":unavailable"), (unavailable ? 1 : 0));
|
||||
|
||||
SongList songs;
|
||||
if (!q.Exec()) {
|
||||
@@ -1277,7 +1276,7 @@ SongList CollectionBackend::GetSongsBySongId(const QStringList &song_ids, QSqlDa
|
||||
for (const QString &song_id : song_ids) {
|
||||
song_ids2 << QLatin1Char('\'') + song_id + QLatin1Char('\'');
|
||||
}
|
||||
QString in = song_ids2.join(u',');
|
||||
QString in = song_ids2.join(QLatin1Char(','));
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE SONG_ID IN (%3)").arg(Song::kRowIdColumnSpec, songs_table_, in));
|
||||
@@ -1304,7 +1303,7 @@ SongList CollectionBackend::GetSongsByFingerprint(const QString &fingerprint) {
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE fingerprint = :fingerprint").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
q.BindValue(u":fingerprint"_s, fingerprint);
|
||||
q.BindValue(QStringLiteral(":fingerprint"), fingerprint);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return SongList();
|
||||
@@ -1332,9 +1331,9 @@ SongList CollectionBackend::GetCompilationSongs(const QString &album, const Coll
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.SetColumnSpec(u"%songs_table.ROWID, "_s + Song::kColumnSpec);
|
||||
query.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec);
|
||||
query.AddCompilationRequirement(true);
|
||||
query.AddWhere(u"album"_s, album);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
@@ -1425,10 +1424,10 @@ bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &cha
|
||||
{ // Get song, so we can tell the model its updated
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = 0").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
q.BindValue(u":url1"_s, url.toString());
|
||||
q.BindValue(u":url2"_s, url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(u":url3"_s, url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(u":url4"_s, url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url1"), url.toString());
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded));
|
||||
if (q.Exec()) {
|
||||
while (q.next()) {
|
||||
Song song(source_);
|
||||
@@ -1446,11 +1445,11 @@ bool CollectionBackend::UpdateCompilations(const QSqlDatabase &db, SongList &cha
|
||||
// Update the song
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET compilation_detected = :compilation_detected, compilation_effective = ((compilation OR :compilation_detected OR compilation_on) AND NOT compilation_off) + 0 WHERE (url = :url1 OR url = :url2 OR url = :url3 OR url = :url4) AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(u":compilation_detected"_s, static_cast<int>(compilation_detected));
|
||||
q.BindValue(u":url1"_s, url.toString());
|
||||
q.BindValue(u":url2"_s, url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(u":url3"_s, url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(u":url4"_s, url.toEncoded(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":compilation_detected"), static_cast<int>(compilation_detected));
|
||||
q.BindValue(QStringLiteral(":url1"), url.toString());
|
||||
q.BindValue(QStringLiteral(":url2"), url.toString(QUrl::FullyEncoded));
|
||||
q.BindValue(QStringLiteral(":url3"), url.toEncoded(QUrl::FullyDecoded));
|
||||
q.BindValue(QStringLiteral(":url4"), url.toEncoded(QUrl::FullyEncoded));
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return false;
|
||||
@@ -1466,15 +1465,15 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
CollectionQuery query(db, songs_table_, opt);
|
||||
query.SetColumnSpec(u"url, filetype, cue_path, effective_albumartist, album, compilation_effective, art_embedded, art_automatic, art_manual, art_unset"_s);
|
||||
query.SetOrderBy(u"effective_albumartist, album, url"_s);
|
||||
query.SetColumnSpec(QStringLiteral("url, filetype, cue_path, effective_albumartist, album, compilation_effective, art_embedded, art_automatic, art_manual, art_unset"));
|
||||
query.SetOrderBy(QStringLiteral("effective_albumartist, album, url"));
|
||||
|
||||
if (compilation_required) {
|
||||
query.AddCompilationRequirement(true);
|
||||
}
|
||||
else if (!artist.isEmpty()) {
|
||||
query.AddCompilationRequirement(false);
|
||||
query.AddWhere(u"effective_albumartist"_s, artist);
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), artist);
|
||||
}
|
||||
|
||||
if (!query.Exec()) {
|
||||
@@ -1502,7 +1501,7 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
|
||||
album_info.art_embedded = query.Value(6).toBool();
|
||||
|
||||
const QString art_automatic = query.Value(7).toString();
|
||||
static const QRegularExpression regex_url_schema(u"..+:.*"_s);
|
||||
static const QRegularExpression regex_url_schema(QStringLiteral("..+:.*"));
|
||||
if (art_automatic.contains(regex_url_schema)) {
|
||||
album_info.art_automatic = QUrl::fromEncoded(art_automatic.toUtf8());
|
||||
}
|
||||
@@ -1525,7 +1524,7 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
|
||||
key.append(album_info.album_artist);
|
||||
}
|
||||
if (!album_info.album.isEmpty()) {
|
||||
if (!key.isEmpty()) key.append(u'-');
|
||||
if (!key.isEmpty()) key.append(QLatin1Char('-'));
|
||||
key.append(album_info.album);
|
||||
}
|
||||
if (!filetype.isEmpty()) {
|
||||
@@ -1558,11 +1557,11 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective
|
||||
ret.album_artist = effective_albumartist;
|
||||
|
||||
CollectionQuery query(db, songs_table_);
|
||||
query.SetColumnSpec(u"url, art_embedded, art_automatic, art_manual, art_unset"_s);
|
||||
query.SetColumnSpec(QStringLiteral("url, art_embedded, art_automatic, art_manual, art_unset"));
|
||||
if (!effective_albumartist.isEmpty()) {
|
||||
query.AddWhere(u"effective_albumartist"_s, effective_albumartist);
|
||||
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
}
|
||||
query.AddWhere(u"album"_s, album);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
@@ -1595,9 +1594,9 @@ void CollectionBackend::UpdateEmbeddedAlbumArt(const QString &effective_albumart
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_embedded = :art_embedded, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(u":art_embedded"_s, art_embedded ? 1 : 0);
|
||||
q.BindValue(u":effective_albumartist"_s, effective_albumartist);
|
||||
q.BindValue(u":album"_s, album);
|
||||
q.BindValue(QStringLiteral(":art_embedded"), art_embedded ? 1 : 0);
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -1608,8 +1607,8 @@ void CollectionBackend::UpdateEmbeddedAlbumArt(const QString &effective_albumart
|
||||
{
|
||||
CollectionQuery q(db, songs_table_);
|
||||
q.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
q.AddWhere(u"effective_albumartist"_s, effective_albumartist);
|
||||
q.AddWhere(u"album"_s, album);
|
||||
q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
q.AddWhere(QStringLiteral("album"), album);
|
||||
if (!q.Exec()) {
|
||||
ReportErrors(q);
|
||||
return;
|
||||
@@ -1641,9 +1640,9 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartis
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_manual = :art_manual, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(u":art_manual"_s, art_manual.isValid() ? art_manual.toString(QUrl::FullyEncoded) : ""_L1);
|
||||
q.BindValue(u":effective_albumartist"_s, effective_albumartist);
|
||||
q.BindValue(u":album"_s, album);
|
||||
q.BindValue(QStringLiteral(":art_manual"), art_manual.isValid() ? art_manual.toString(QUrl::FullyEncoded) : QLatin1String(""));
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -1654,8 +1653,8 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartis
|
||||
{
|
||||
CollectionQuery q(db, songs_table_);
|
||||
q.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
q.AddWhere(u"effective_albumartist"_s, effective_albumartist);
|
||||
q.AddWhere(u"album"_s, album);
|
||||
q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
q.AddWhere(QStringLiteral("album"), album);
|
||||
if (!q.Exec()) {
|
||||
ReportErrors(q);
|
||||
return;
|
||||
@@ -1687,8 +1686,8 @@ void CollectionBackend::UnsetAlbumArt(const QString &effective_albumartist, cons
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_unset = 1, art_manual = '', art_automatic = '', art_embedded = '' WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(u":effective_albumartist"_s, effective_albumartist);
|
||||
q.BindValue(u":album"_s, album);
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -1699,8 +1698,8 @@ void CollectionBackend::UnsetAlbumArt(const QString &effective_albumartist, cons
|
||||
{
|
||||
CollectionQuery q(db, songs_table_);
|
||||
q.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
q.AddWhere(u"effective_albumartist"_s, effective_albumartist);
|
||||
q.AddWhere(u"album"_s, album);
|
||||
q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
q.AddWhere(QStringLiteral("album"), album);
|
||||
if (!q.Exec()) {
|
||||
ReportErrors(q);
|
||||
return;
|
||||
@@ -1732,9 +1731,9 @@ void CollectionBackend::ClearAlbumArt(const QString &effective_albumartist, cons
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET art_embedded = 0, art_automatic = '', art_manual = '', art_unset = :art_unset WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(u":art_unset"_s, art_unset ? 1 : 0);
|
||||
q.BindValue(u":effective_albumartist"_s, effective_albumartist);
|
||||
q.BindValue(u":album"_s, album);
|
||||
q.BindValue(QStringLiteral(":art_unset"), art_unset ? 1 : 0);
|
||||
q.BindValue(QStringLiteral(":effective_albumartist"), effective_albumartist);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -1745,8 +1744,8 @@ void CollectionBackend::ClearAlbumArt(const QString &effective_albumartist, cons
|
||||
{
|
||||
CollectionQuery q(db, songs_table_);
|
||||
q.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
q.AddWhere(u"effective_albumartist"_s, effective_albumartist);
|
||||
q.AddWhere(u"album"_s, album);
|
||||
q.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
|
||||
q.AddWhere(QStringLiteral("album"), album);
|
||||
if (!q.Exec()) {
|
||||
ReportErrors(q);
|
||||
return;
|
||||
@@ -1774,14 +1773,14 @@ void CollectionBackend::ForceCompilation(const QString &album, const QStringList
|
||||
|
||||
// Update the songs
|
||||
QString sql(QStringLiteral("UPDATE %1 SET compilation_on = :compilation_on, compilation_off = :compilation_off, compilation_effective = ((compilation OR compilation_detected OR :compilation_on) AND NOT :compilation_off) + 0 WHERE album = :album AND unavailable = 0").arg(songs_table_));
|
||||
if (!artist.isEmpty()) sql += " AND artist = :artist"_L1;
|
||||
if (!artist.isEmpty()) sql += QLatin1String(" AND artist = :artist");
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(sql);
|
||||
q.BindValue(u":compilation_on"_s, on ? 1 : 0);
|
||||
q.BindValue(u":compilation_off"_s, on ? 0 : 1);
|
||||
q.BindValue(u":album"_s, album);
|
||||
if (!artist.isEmpty()) q.BindValue(u":artist"_s, artist);
|
||||
q.BindValue(QStringLiteral(":compilation_on"), on ? 1 : 0);
|
||||
q.BindValue(QStringLiteral(":compilation_off"), on ? 0 : 1);
|
||||
q.BindValue(QStringLiteral(":album"), album);
|
||||
if (!artist.isEmpty()) q.BindValue(QStringLiteral(":artist"), artist);
|
||||
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
@@ -1792,8 +1791,8 @@ void CollectionBackend::ForceCompilation(const QString &album, const QStringList
|
||||
|
||||
CollectionQuery query(db, songs_table_);
|
||||
query.SetColumnSpec(Song::kRowIdColumnSpec);
|
||||
query.AddWhere(u"album"_s, album);
|
||||
if (!artist.isEmpty()) query.AddWhere(u"artist"_s, artist);
|
||||
query.AddWhere(QStringLiteral("album"), album);
|
||||
if (!artist.isEmpty()) query.AddWhere(QStringLiteral("artist"), artist);
|
||||
|
||||
if (!query.Exec()) {
|
||||
ReportErrors(query);
|
||||
@@ -1822,8 +1821,8 @@ void CollectionBackend::IncrementPlayCount(const int id) {
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET playcount = playcount + 1, lastplayed = :now WHERE ROWID = :id").arg(songs_table_));
|
||||
q.BindValue(u":now"_s, QDateTime::currentSecsSinceEpoch());
|
||||
q.BindValue(u":id"_s, id);
|
||||
q.BindValue(QStringLiteral(":now"), QDateTime::currentSecsSinceEpoch());
|
||||
q.BindValue(QStringLiteral(":id"), id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -1845,7 +1844,7 @@ void CollectionBackend::IncrementSkipCount(const int id, const float progress) {
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET skipcount = skipcount + 1 WHERE ROWID = :id").arg(songs_table_));
|
||||
q.BindValue(u":id"_s, id);
|
||||
q.BindValue(QStringLiteral(":id"), id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -1891,7 +1890,7 @@ bool CollectionBackend::ResetPlayStatistics(const QStringList &id_str_list) {
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID IN (:ids)").arg(songs_table_));
|
||||
q.BindValue(u":ids"_s, id_str_list.join(u','));
|
||||
q.BindValue(QStringLiteral(":ids"), id_str_list.join(QLatin1Char(',')));
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return false;
|
||||
@@ -1916,7 +1915,7 @@ void CollectionBackend::DeleteAll() {
|
||||
|
||||
{
|
||||
SqlQuery q(db);
|
||||
q.prepare(u"DELETE FROM "_s + songs_table_);
|
||||
q.prepare(QStringLiteral("DELETE FROM ") + songs_table_);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -1930,26 +1929,37 @@ void CollectionBackend::DeleteAll() {
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionBackend::ExecuteQuery(const QString &sql) {
|
||||
SongList CollectionBackend::SmartPlaylistsFindSongs(const SmartPlaylistSearch &search) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
// Build the query
|
||||
QString sql = search.ToSql(songs_table());
|
||||
|
||||
// Run the query
|
||||
SongList ret;
|
||||
SqlQuery query(db);
|
||||
query.prepare(sql);
|
||||
if (!query.Exec()) {
|
||||
db_->ReportErrors(query);
|
||||
return SongList();
|
||||
return ret;
|
||||
}
|
||||
|
||||
SongList songs;
|
||||
// Read the results
|
||||
while (query.next()) {
|
||||
Song song;
|
||||
song.InitFromQuery(query, true);
|
||||
songs << song;
|
||||
ret << song;
|
||||
}
|
||||
return ret;
|
||||
|
||||
return songs;
|
||||
}
|
||||
|
||||
SongList CollectionBackend::SmartPlaylistsGetAllSongs() {
|
||||
|
||||
// Get all the songs!
|
||||
return SmartPlaylistsFindSongs(SmartPlaylistSearch(SmartPlaylistSearch::SearchType::All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::SortType::FieldAsc, SmartPlaylistSearchTerm::Field::Artist, -1));
|
||||
|
||||
}
|
||||
|
||||
@@ -1966,9 +1976,9 @@ SongList CollectionBackend::GetSongsBy(const QString &artist, const QString &alb
|
||||
else {
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 WHERE artist = :artist COLLATE NOCASE AND album = :album COLLATE NOCASE AND title = :title COLLATE NOCASE").arg(Song::kRowIdColumnSpec, songs_table_));
|
||||
}
|
||||
q.BindValue(u":artist"_s, artist);
|
||||
if (!album.isEmpty()) q.BindValue(u":album"_s, album);
|
||||
q.BindValue(u":title"_s, title);
|
||||
q.BindValue(QStringLiteral(":artist"), artist);
|
||||
if (!album.isEmpty()) q.BindValue(QStringLiteral(":album"), album);
|
||||
q.BindValue(QStringLiteral(":title"), title);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return SongList();
|
||||
@@ -2000,8 +2010,8 @@ void CollectionBackend::UpdateLastPlayed(const QString &artist, const QString &a
|
||||
}
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET lastplayed = :lastplayed WHERE ROWID = :id").arg(songs_table_));
|
||||
q.BindValue(u":lastplayed"_s, lastplayed);
|
||||
q.BindValue(u":id"_s, song.id());
|
||||
q.BindValue(QStringLiteral(":lastplayed"), lastplayed);
|
||||
q.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
continue;
|
||||
@@ -2026,8 +2036,8 @@ void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &ti
|
||||
for (const Song &song : songs) {
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET playcount = :playcount WHERE ROWID = :id").arg(songs_table_));
|
||||
q.BindValue(u":playcount"_s, playcount);
|
||||
q.BindValue(u":id"_s, song.id());
|
||||
q.BindValue(QStringLiteral(":playcount"), playcount);
|
||||
q.BindValue(QStringLiteral(":id"), song.id());
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -2058,10 +2068,10 @@ void CollectionBackend::UpdateSongsRating(const QList<int> &id_list, const float
|
||||
for (int i : id_list) {
|
||||
id_str_list << QString::number(i);
|
||||
}
|
||||
QString ids = id_str_list.join(u',');
|
||||
QString ids = id_str_list.join(QLatin1Char(','));
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET rating = :rating WHERE ROWID IN (%2)").arg(songs_table_, ids));
|
||||
q.BindValue(u":rating"_s, rating);
|
||||
q.BindValue(QStringLiteral(":rating"), rating);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -2089,8 +2099,8 @@ void CollectionBackend::UpdateLastSeen(const int directory_id, const int expire_
|
||||
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("UPDATE %1 SET lastseen = :lastseen WHERE directory_id = :directory_id AND unavailable = 0").arg(songs_table_));
|
||||
q.BindValue(u":lastseen"_s, QDateTime::currentSecsSinceEpoch());
|
||||
q.BindValue(u":directory_id"_s, directory_id);
|
||||
q.BindValue(QStringLiteral(":lastseen"), QDateTime::currentSecsSinceEpoch());
|
||||
q.BindValue(QStringLiteral(":directory_id"), directory_id);
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -2109,8 +2119,8 @@ void CollectionBackend::ExpireSongs(const int directory_id, const int expire_una
|
||||
QSqlDatabase db(db_->Connect());
|
||||
SqlQuery q(db);
|
||||
q.prepare(QStringLiteral("SELECT %1 FROM %2 LEFT JOIN playlist_items ON %2.ROWID = playlist_items.collection_id WHERE %2.directory_id = :directory_id AND %2.unavailable = 1 AND %2.lastseen > 0 AND %2.lastseen < :time AND playlist_items.collection_id IS NULL").arg(Song::JoinSpec(songs_table_), songs_table_));
|
||||
q.BindValue(u":directory_id"_s, directory_id);
|
||||
q.BindValue(u":time"_s, QDateTime::currentSecsSinceEpoch() - (expire_unavailable_songs_days * 86400LL));
|
||||
q.BindValue(QStringLiteral(":directory_id"), directory_id);
|
||||
q.BindValue(QStringLiteral(":time"), QDateTime::currentSecsSinceEpoch() - (expire_unavailable_songs_days * 86400LL));
|
||||
if (!q.Exec()) {
|
||||
db_->ReportErrors(q);
|
||||
return;
|
||||
@@ -2125,3 +2135,5 @@ void CollectionBackend::ExpireSongs(const int directory_id, const int expire_una
|
||||
if (!songs.isEmpty()) DeleteSongs(songs);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
#include <QUrl>
|
||||
#include <QSqlDatabase>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/song.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionquery.h"
|
||||
@@ -45,6 +45,7 @@
|
||||
class QThread;
|
||||
class TaskManager;
|
||||
class Database;
|
||||
class SmartPlaylistSearch;
|
||||
|
||||
class CollectionBackendInterface : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -226,7 +227,8 @@ class CollectionBackend : public CollectionBackendInterface {
|
||||
|
||||
SongList GetSongsByFingerprint(const QString &fingerprint) override;
|
||||
|
||||
SongList ExecuteQuery(const QString &sql);
|
||||
SongList SmartPlaylistsGetAllSongs();
|
||||
SongList SmartPlaylistsFindSongs(const SmartPlaylistSearch &search);
|
||||
|
||||
void AddOrUpdateSongsAsync(const SongList &songs);
|
||||
void UpdateSongsBySongIDAsync(const SongMap &new_songs);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/filesystemmusicstorage.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/musicstorage.h"
|
||||
@@ -39,11 +39,10 @@
|
||||
#include "collectiondirectorymodel.h"
|
||||
|
||||
using std::make_shared;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
CollectionDirectoryModel::CollectionDirectoryModel(SharedPtr<CollectionBackend> backend, QObject *parent)
|
||||
: QStandardItemModel(parent),
|
||||
dir_icon_(IconLoader::Load(u"document-open-folder"_s)),
|
||||
dir_icon_(IconLoader::Load(QStringLiteral("document-open-folder"))),
|
||||
backend_(backend) {
|
||||
|
||||
QObject::connect(&*backend_, &CollectionBackend::DirectoryAdded, this, &CollectionDirectoryModel::AddDirectory);
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#include <QStringList>
|
||||
#include <QIcon>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "collectiondirectory.h"
|
||||
|
||||
class QModelIndex;
|
||||
|
||||
@@ -28,9 +28,10 @@
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "core/songmimedata.h"
|
||||
#include "filterparser/filterparser.h"
|
||||
#include "filterparser/filtertree.h"
|
||||
#include "playlist/songmimedata.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "collectionfilter.h"
|
||||
#include "collectionmodel.h"
|
||||
@@ -59,7 +60,11 @@ bool CollectionFilter::filterAcceptsRow(const int source_row, const QModelIndex
|
||||
return item->type == CollectionItem::Type::LoadingIndicator;
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
size_t hash = qHash(filter_string_);
|
||||
#else
|
||||
uint hash = qHash(filter_string_);
|
||||
#endif
|
||||
if (hash != query_hash_) {
|
||||
FilterParser p(filter_string_);
|
||||
filter_tree_.reset(p.parse());
|
||||
@@ -94,7 +99,7 @@ QMimeData *CollectionFilter::mimeData(const QModelIndexList &indexes) const {
|
||||
}
|
||||
|
||||
data->setUrls(urls);
|
||||
data->name_for_new_playlist_ = Song::GetNameForNewPlaylist(data->songs);
|
||||
data->name_for_new_playlist_ = PlaylistManager::GetNameForNewPlaylist(data->songs);
|
||||
|
||||
return data;
|
||||
|
||||
|
||||
@@ -51,7 +51,11 @@ class CollectionFilter : public QSortFilterProxyModel {
|
||||
|
||||
private:
|
||||
mutable QScopedPointer<FilterTree> filter_tree_;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
mutable size_t query_hash_;
|
||||
#else
|
||||
mutable uint query_hash_;
|
||||
#endif
|
||||
QString filter_string_;
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <utility>
|
||||
#include <memory>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QWidget>
|
||||
@@ -41,24 +42,24 @@
|
||||
#include <QMenu>
|
||||
#include <QSettings>
|
||||
#include <QToolButton>
|
||||
#include <QKeyEvent>
|
||||
#include <QtEvents>
|
||||
|
||||
#include "core/iconloader.h"
|
||||
#include "core/song.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/settings.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionfilter.h"
|
||||
#include "collectionquery.h"
|
||||
#include "filterparser/filterparser.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "groupbydialog.h"
|
||||
#include "ui_collectionfilterwidget.h"
|
||||
#include "widgets/searchfield.h"
|
||||
#include "constants/collectionsettings.h"
|
||||
#include "constants/appearancesettings.h"
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
#include "settings/collectionsettingspage.h"
|
||||
#include "settings/appearancesettingspage.h"
|
||||
|
||||
namespace {
|
||||
constexpr int kFilterDelay = 500; // msec
|
||||
@@ -90,7 +91,7 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
||||
timer_filter_delay_->setSingleShot(true);
|
||||
|
||||
// Icons
|
||||
ui_->options->setIcon(IconLoader::Load(u"configure"_s));
|
||||
ui_->options->setIcon(IconLoader::Load(QStringLiteral("configure")));
|
||||
|
||||
// Filter by age
|
||||
QActionGroup *filter_age_group = new QActionGroup(this);
|
||||
@@ -204,8 +205,8 @@ void CollectionFilterWidget::setFilter(CollectionFilter *filter) {
|
||||
void CollectionFilterWidget::ReloadSettings() {
|
||||
|
||||
Settings s;
|
||||
s.beginGroup(AppearanceSettings::kSettingsGroup);
|
||||
int iconsize = s.value(AppearanceSettings::kIconSizeConfigureButtons, 20).toInt();
|
||||
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
|
||||
int iconsize = s.value(AppearanceSettingsPage::kIconSizeConfigureButtons, 20).toInt();
|
||||
s.endGroup();
|
||||
ui_->options->setIconSize(QSize(iconsize, iconsize));
|
||||
ui_->search_field->setIconSize(iconsize);
|
||||
@@ -215,7 +216,7 @@ void CollectionFilterWidget::ReloadSettings() {
|
||||
QString CollectionFilterWidget::group_by_version() const {
|
||||
|
||||
if (settings_prefix_.isEmpty()) {
|
||||
return u"group_by_version"_s;
|
||||
return QStringLiteral("group_by_version");
|
||||
}
|
||||
|
||||
return QStringLiteral("%1_group_by_version").arg(settings_prefix_);
|
||||
@@ -225,7 +226,7 @@ QString CollectionFilterWidget::group_by_version() const {
|
||||
QString CollectionFilterWidget::group_by_key() const {
|
||||
|
||||
if (settings_prefix_.isEmpty()) {
|
||||
return u"group_by"_s;
|
||||
return QStringLiteral("group_by");
|
||||
}
|
||||
|
||||
return QStringLiteral("%1_group_by").arg(settings_prefix_);
|
||||
@@ -237,7 +238,7 @@ QString CollectionFilterWidget::group_by_key(const int number) const { return gr
|
||||
QString CollectionFilterWidget::separate_albums_by_grouping_key() const {
|
||||
|
||||
if (settings_prefix_.isEmpty()) {
|
||||
return u"separate_albums_by_grouping"_s;
|
||||
return QStringLiteral("separate_albums_by_grouping");
|
||||
}
|
||||
|
||||
return QStringLiteral("%1_separate_albums_by_grouping").arg(settings_prefix_);
|
||||
@@ -295,7 +296,7 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
|
||||
if (version == 1) {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
if (saved.at(i) == QLatin1String("version")) continue;
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
@@ -306,7 +307,7 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
|
||||
else {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
if (saved.at(i) == QLatin1String("version")) continue;
|
||||
s.remove(saved.at(i));
|
||||
}
|
||||
}
|
||||
@@ -345,7 +346,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
||||
qLog(Debug) << "Saving current grouping to" << name;
|
||||
|
||||
Settings s;
|
||||
if (settings_group_.isEmpty() || settings_group_ == QLatin1String(CollectionSettings::kSettingsGroup)) {
|
||||
if (settings_group_.isEmpty() || settings_group_ == QLatin1String(CollectionSettingsPage::kSettingsGroup)) {
|
||||
s.beginGroup(SavedGroupingManager::kSavedGroupingsSettingsGroup);
|
||||
}
|
||||
else {
|
||||
@@ -354,7 +355,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
||||
QByteArray buffer;
|
||||
QDataStream datastream(&buffer, QIODevice::WriteOnly);
|
||||
datastream << model_->GetGroupBy();
|
||||
s.setValue("version", u"1"_s);
|
||||
s.setValue("version", QStringLiteral("1"));
|
||||
s.setValue(name, buffer);
|
||||
s.endGroup();
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QWidget>
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "collectionitem.h"
|
||||
|
||||
CollectionItem::CollectionItem(SimpleTreeModel<CollectionItem> *_model)
|
||||
: SimpleTreeItem<CollectionItem>(_model),
|
||||
type(Type::Root),
|
||||
container_level(-1),
|
||||
compilation_artist_node_(nullptr) {}
|
||||
|
||||
CollectionItem::CollectionItem(const Type _type, CollectionItem *_parent)
|
||||
: SimpleTreeItem<CollectionItem>(_parent),
|
||||
type(_type),
|
||||
container_level(-1),
|
||||
compilation_artist_node_(nullptr) {}
|
||||
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, 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
|
||||
@@ -20,6 +22,8 @@
|
||||
#ifndef COLLECTIONITEM_H
|
||||
#define COLLECTIONITEM_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "core/simpletreeitem.h"
|
||||
#include "core/song.h"
|
||||
|
||||
@@ -33,8 +37,17 @@ class CollectionItem : public SimpleTreeItem<CollectionItem> {
|
||||
LoadingIndicator,
|
||||
};
|
||||
|
||||
explicit CollectionItem(SimpleTreeModel<CollectionItem> *_model);
|
||||
explicit CollectionItem(const Type _type, CollectionItem *_parent = nullptr);
|
||||
explicit CollectionItem(SimpleTreeModel<CollectionItem> *_model)
|
||||
: SimpleTreeItem<CollectionItem>(_model),
|
||||
type(Type::Root),
|
||||
container_level(-1),
|
||||
compilation_artist_node_(nullptr) {}
|
||||
|
||||
explicit CollectionItem(const Type _type, CollectionItem *_parent = nullptr)
|
||||
: SimpleTreeItem<CollectionItem>(_parent),
|
||||
type(_type),
|
||||
container_level(-1),
|
||||
compilation_artist_node_(nullptr) {}
|
||||
|
||||
Type type;
|
||||
int container_level;
|
||||
|
||||
@@ -50,16 +50,17 @@
|
||||
#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/scoped_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/application.h"
|
||||
#include "core/database.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/sqlrow.h"
|
||||
#include "core/settings.h"
|
||||
#include "core/songmimedata.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "collectionquery.h"
|
||||
#include "collectionbackend.h"
|
||||
@@ -68,13 +69,14 @@
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionmodelupdate.h"
|
||||
#include "collectionfilter.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "playlist/songmimedata.h"
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
#include "covermanager/albumcoverloader.h"
|
||||
#include "constants/collectionsettings.h"
|
||||
#include "settings/collectionsettingspage.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
const int CollectionModel::kPrettyCoverSize = 32;
|
||||
namespace {
|
||||
@@ -82,39 +84,44 @@ constexpr char kPixmapDiskCacheDir[] = "pixmapcache";
|
||||
constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists");
|
||||
} // namespace
|
||||
|
||||
CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, const SharedPtr<AlbumCoverLoader> albumcover_loader, QObject *parent)
|
||||
QNetworkDiskCache *CollectionModel::sIconCache = nullptr;
|
||||
|
||||
CollectionModel::CollectionModel(SharedPtr<CollectionBackend> backend, Application *app, QObject *parent)
|
||||
: SimpleTreeModel<CollectionItem>(new CollectionItem(this), parent),
|
||||
backend_(backend),
|
||||
albumcover_loader_(albumcover_loader),
|
||||
app_(app),
|
||||
dir_model_(new CollectionDirectoryModel(backend, this)),
|
||||
filter_(new CollectionFilter(this)),
|
||||
timer_reload_(new QTimer(this)),
|
||||
timer_update_(new QTimer(this)),
|
||||
icon_artist_(IconLoader::Load(u"folder-sound"_s)),
|
||||
icon_artist_(IconLoader::Load(QStringLiteral("folder-sound"))),
|
||||
use_disk_cache_(false),
|
||||
total_song_count_(0),
|
||||
total_artist_count_(0),
|
||||
total_album_count_(0),
|
||||
loading_(false),
|
||||
icon_disk_cache_(new QNetworkDiskCache(this)) {
|
||||
loading_(false) {
|
||||
|
||||
setObjectName(backend_->source() == Song::Source::Collection ? QLatin1String(QObject::metaObject()->className()) : QStringLiteral("%1%2").arg(Song::DescriptionForSource(backend_->source()), QLatin1String(QObject::metaObject()->className())));
|
||||
setObjectName(backend_->source() == Song::Source::Collection ? QLatin1String(metaObject()->className()) : QStringLiteral("%1%2").arg(Song::DescriptionForSource(backend_->source()), QLatin1String(metaObject()->className())));
|
||||
|
||||
filter_->setSourceModel(this);
|
||||
filter_->setSortRole(Role_SortText);
|
||||
filter_->sort(0);
|
||||
|
||||
if (albumcover_loader_) {
|
||||
QObject::connect(&*albumcover_loader_, &AlbumCoverLoader::AlbumCoverLoaded, this, &CollectionModel::AlbumCoverLoaded);
|
||||
if (app_) {
|
||||
QObject::connect(&*app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &CollectionModel::AlbumCoverLoaded);
|
||||
}
|
||||
|
||||
QIcon nocover = IconLoader::Load(u"cdcase"_s);
|
||||
QIcon nocover = IconLoader::Load(QStringLiteral("cdcase"));
|
||||
if (!nocover.isNull()) {
|
||||
QList<QSize> nocover_sizes = nocover.availableSizes();
|
||||
pixmap_no_cover_ = nocover.pixmap(nocover_sizes.last()).scaled(kPrettyCoverSize, kPrettyCoverSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
icon_disk_cache_->setCacheDirectory(StandardPaths::WritableLocation(StandardPaths::StandardLocation::CacheLocation) + u'/' + QLatin1String(kPixmapDiskCacheDir) + u'-' + Song::TextForSource(backend_->source()));
|
||||
if (app_ && !sIconCache) {
|
||||
sIconCache = new QNetworkDiskCache(this);
|
||||
sIconCache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') + QLatin1String(kPixmapDiskCacheDir));
|
||||
QObject::connect(app_, &Application::ClearPixmapDiskCache, this, &CollectionModel::ClearDiskCache);
|
||||
}
|
||||
|
||||
QObject::connect(&*backend_, &CollectionBackend::SongsAdded, this, &CollectionModel::AddReAddOrUpdate);
|
||||
QObject::connect(&*backend_, &CollectionBackend::SongsChanged, this, &CollectionModel::AddReAddOrUpdate);
|
||||
@@ -222,16 +229,16 @@ void CollectionModel::ScheduleReset() {
|
||||
void CollectionModel::ReloadSettings() {
|
||||
|
||||
Settings settings;
|
||||
settings.beginGroup(CollectionSettings::kSettingsGroup);
|
||||
const bool show_pretty_covers = settings.value(CollectionSettings::kPrettyCovers, true).toBool();
|
||||
const bool show_dividers= settings.value(CollectionSettings::kShowDividers, true).toBool();
|
||||
const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool();
|
||||
const bool sort_skips_articles = settings.value(CollectionSettings::kSortSkipsArticles, true).toBool();
|
||||
settings.beginGroup(CollectionSettingsPage::kSettingsGroup);
|
||||
const bool show_pretty_covers = settings.value("pretty_covers", true).toBool();
|
||||
const bool show_dividers= settings.value("show_dividers", true).toBool();
|
||||
const bool show_various_artists = settings.value("various_artists", true).toBool();
|
||||
const bool sort_skips_articles = settings.value("sort_skips_articles", true).toBool();
|
||||
|
||||
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
|
||||
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
|
||||
if (icon_disk_cache_) {
|
||||
icon_disk_cache_->setMaximumCacheSize(MaximumCacheSize(&settings, CollectionSettings::kSettingsDiskCacheSize, CollectionSettings::kSettingsDiskCacheSizeUnit, CollectionSettings::kSettingsDiskCacheSizeDefault));
|
||||
use_disk_cache_ = settings.value(CollectionSettingsPage::kSettingsDiskCacheEnable, false).toBool();
|
||||
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettingsPage::kSettingsCacheSize, CollectionSettingsPage::kSettingsCacheSizeUnit, CollectionSettingsPage::kSettingsCacheSizeDefault) / 1024));
|
||||
if (sIconCache) {
|
||||
sIconCache->setMaximumCacheSize(MaximumCacheSize(&settings, CollectionSettingsPage::kSettingsDiskCacheSize, CollectionSettingsPage::kSettingsDiskCacheSizeUnit, CollectionSettingsPage::kSettingsDiskCacheSizeDefault));
|
||||
}
|
||||
|
||||
settings.endGroup();
|
||||
@@ -250,7 +257,7 @@ void CollectionModel::ReloadSettings() {
|
||||
}
|
||||
|
||||
if (!use_disk_cache_) {
|
||||
ClearIconDiskCache();
|
||||
ClearDiskCache();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -288,11 +295,29 @@ void CollectionModel::SetFilterMaxAge(const int filter_max_age) {
|
||||
|
||||
QVariant CollectionModel::data(const QModelIndex &idx, const int role) const {
|
||||
|
||||
return data(IndexToItem(idx), role);
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
QVariant CollectionModel::data(CollectionItem *item, const int role) const {
|
||||
QVariant CollectionModel::data(const CollectionItem *item, const int role) const {
|
||||
|
||||
GroupBy container_group_by = item->type == CollectionItem::Type::Container ? options_active_.group_by[item->container_level] : GroupBy::None;
|
||||
|
||||
@@ -311,7 +336,7 @@ QVariant CollectionModel::data(CollectionItem *item, const int role) const {
|
||||
case GroupBy::YearAlbumDisc:
|
||||
case GroupBy::OriginalYearAlbum:
|
||||
case GroupBy::OriginalYearAlbumDisc:
|
||||
return options_active_.show_pretty_covers ? const_cast<CollectionModel*>(this)->AlbumIcon(item) : QVariant();
|
||||
return QVariant();
|
||||
case GroupBy::Artist:
|
||||
case GroupBy::AlbumArtist:
|
||||
return icon_artist_;
|
||||
@@ -383,25 +408,25 @@ Qt::ItemFlags CollectionModel::flags(const QModelIndex &idx) const {
|
||||
}
|
||||
|
||||
QStringList CollectionModel::mimeTypes() const {
|
||||
return QStringList() << u"text/uri-list"_s;
|
||||
return QStringList() << QStringLiteral("text/uri-list");
|
||||
}
|
||||
|
||||
QMimeData *CollectionModel::mimeData(const QModelIndexList &indexes) const {
|
||||
|
||||
if (indexes.isEmpty()) return nullptr;
|
||||
|
||||
SongList songs;
|
||||
QSet<int> song_ids;
|
||||
SongMimeData *data = new SongMimeData;
|
||||
QList<QUrl> urls;
|
||||
QSet<int> song_ids;
|
||||
|
||||
data->backend = backend_;
|
||||
|
||||
for (const QModelIndex &idx : indexes) {
|
||||
GetChildSongs(IndexToItem(idx), songs, song_ids, urls);
|
||||
GetChildSongs(IndexToItem(idx), &urls, &data->songs, &song_ids);
|
||||
}
|
||||
|
||||
SongMimeData *data = new SongMimeData;
|
||||
data->setUrls(urls);
|
||||
data->backend = backend_;
|
||||
data->songs = songs;
|
||||
data->name_for_new_playlist_ = Song::GetNameForNewPlaylist(data->songs);
|
||||
data->name_for_new_playlist_ = PlaylistManager::GetNameForNewPlaylist(data->songs);
|
||||
|
||||
return data;
|
||||
|
||||
@@ -560,7 +585,7 @@ void CollectionModel::AddSongsInternal(const SongList &songs) {
|
||||
container_key = container->container_key;
|
||||
}
|
||||
else {
|
||||
if (!container_key.isEmpty()) container_key.append(u'-');
|
||||
if (!container_key.isEmpty()) container_key.append(QLatin1Char('-'));
|
||||
container_key.append(ContainerKey(group_by, song, has_unique_album_identifier));
|
||||
if (container_nodes_[i].contains(container_key)) {
|
||||
container = container_nodes_[i][container_key];
|
||||
@@ -699,7 +724,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
||||
|
||||
QString divider_key;
|
||||
if (options_active_.show_dividers && container_level == 0) {
|
||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skips_articles));
|
||||
divider_key = DividerKey(group_by, song, SortText(group_by, container_level, song, options_active_.sort_skips_articles));
|
||||
if (!divider_key.isEmpty()) {
|
||||
if (!divider_nodes_.contains(divider_key)) {
|
||||
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
|
||||
@@ -713,7 +738,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
||||
item->container_level = container_level;
|
||||
item->container_key = container_key;
|
||||
item->display_text = DisplayText(group_by, song);
|
||||
item->sort_text = SortText(group_by, song, options_active_.sort_skips_articles);
|
||||
item->sort_text = SortText(group_by, container_level, song, options_active_.sort_skips_articles);
|
||||
if (!divider_key.isEmpty()) {
|
||||
item->sort_text.prepend(divider_key + QLatin1Char(' '));
|
||||
}
|
||||
@@ -733,7 +758,7 @@ void CollectionModel::CreateDividerItem(const QString ÷r_key, const QStrin
|
||||
CollectionItem *divider = new CollectionItem(CollectionItem::Type::Divider, root_);
|
||||
divider->container_key = divider_key;
|
||||
divider->display_text = display_text;
|
||||
divider->sort_text = divider_key + " "_L1;
|
||||
divider->sort_text = divider_key + QLatin1String(" ");
|
||||
divider_nodes_[divider_key] = divider;
|
||||
|
||||
endInsertRows();
|
||||
@@ -752,10 +777,16 @@ void CollectionModel::CreateSongItem(const Song &song, CollectionItem *parent) {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::SetSongItemData(CollectionItem *item, const Song &song) const {
|
||||
void CollectionModel::SetSongItemData(CollectionItem *item, const Song &song) {
|
||||
|
||||
item->display_text = song.TitleWithCompilationArtist();
|
||||
item->sort_text = HasParentAlbumGroupBy(item->parent) ? SortTextForSong(song) : SortText(song.title());
|
||||
if (item->container_level == 1 && !IsAlbumGroupBy(options_active_.group_by[0])) {
|
||||
item->sort_text = SortText(song.title());
|
||||
}
|
||||
else {
|
||||
item->sort_text = SortTextForSong(song);
|
||||
}
|
||||
|
||||
item->metadata = song;
|
||||
|
||||
}
|
||||
@@ -771,7 +802,7 @@ CollectionItem *CollectionModel::CreateCompilationArtistNode(CollectionItem *par
|
||||
if (parent != root_ && !parent->container_key.isEmpty()) parent->compilation_artist_node_->container_key.append(parent->container_key);
|
||||
parent->compilation_artist_node_->container_key.append(QLatin1String(kVariousArtists));
|
||||
parent->compilation_artist_node_->display_text = QLatin1String(kVariousArtists);
|
||||
parent->compilation_artist_node_->sort_text = " various"_L1;
|
||||
parent->compilation_artist_node_->sort_text = QLatin1String(" various");
|
||||
parent->compilation_artist_node_->container_level = parent->container_level + 1;
|
||||
|
||||
endInsertRows();
|
||||
@@ -782,7 +813,11 @@ CollectionItem *CollectionModel::CreateCompilationArtistNode(CollectionItem *par
|
||||
|
||||
void CollectionModel::LoadSongsFromSqlAsync() {
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QFuture<SongList> future = QtConcurrent::run(&CollectionModel::LoadSongsFromSql, this, options_active_.filter_options);
|
||||
#else
|
||||
QFuture<SongList> future = QtConcurrent::run(this, &CollectionModel::LoadSongsFromSql, options_active_.filter_options);
|
||||
#endif
|
||||
QFutureWatcher<SongList> *watcher = new QFutureWatcher<SongList>();
|
||||
QObject::connect(watcher, &QFutureWatcher<void>::finished, this, &CollectionModel::LoadSongsFromSqlAsyncFinished);
|
||||
watcher->setFuture(future);
|
||||
@@ -797,7 +832,7 @@ SongList CollectionModel::LoadSongsFromSql(const CollectionFilterOptions &filter
|
||||
QMutexLocker l(backend_->db()->Mutex());
|
||||
QSqlDatabase db(backend_->db()->Connect());
|
||||
CollectionQuery q(db, backend_->songs_table(), filter_options);
|
||||
q.SetColumnSpec(u"%songs_table.ROWID, "_s + Song::kColumnSpec);
|
||||
q.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec);
|
||||
if (q.Exec()) {
|
||||
while (q.Next()) {
|
||||
Song song;
|
||||
@@ -836,9 +871,9 @@ void CollectionModel::LoadSongsFromSqlAsyncFinished() {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::AlbumIconPixmapCacheKey(const CollectionItem *item) const {
|
||||
QString CollectionModel::AlbumIconPixmapCacheKey(const QModelIndex &idx) const {
|
||||
|
||||
return Song::TextForSource(backend_->source()) + QLatin1Char('/') + item->container_key;
|
||||
return Song::TextForSource(backend_->source()) + QLatin1Char('/') + idx.data(Role_ContainerKey).toString();
|
||||
|
||||
}
|
||||
|
||||
@@ -851,9 +886,9 @@ QUrl CollectionModel::AlbumIconPixmapDiskCacheKey(const QString &cache_key) {
|
||||
void CollectionModel::ClearItemPixmapCache(CollectionItem *item) {
|
||||
|
||||
// Remove from pixmap cache
|
||||
const QString cache_key = AlbumIconPixmapCacheKey(item);
|
||||
const QString cache_key = AlbumIconPixmapCacheKey(ItemToIndex(item));
|
||||
QPixmapCache::remove(cache_key);
|
||||
if (use_disk_cache_ && icon_disk_cache_) icon_disk_cache_->remove(AlbumIconPixmapDiskCacheKey(cache_key));
|
||||
if (use_disk_cache_ && sIconCache) sIconCache->remove(AlbumIconPixmapDiskCacheKey(cache_key));
|
||||
if (pending_cache_keys_.contains(cache_key)) {
|
||||
pending_cache_keys_.remove(cache_key);
|
||||
}
|
||||
@@ -870,12 +905,13 @@ void CollectionModel::ClearItemPixmapCache(CollectionItem *item) {
|
||||
|
||||
}
|
||||
|
||||
QVariant CollectionModel::AlbumIcon(CollectionItem *item) {
|
||||
QVariant CollectionModel::AlbumIcon(const QModelIndex &idx) {
|
||||
|
||||
CollectionItem *item = IndexToItem(idx);
|
||||
if (!item) return pixmap_no_cover_;
|
||||
|
||||
// Check the cache for a pixmap we already loaded.
|
||||
const QString cache_key = AlbumIconPixmapCacheKey(item);
|
||||
const QString cache_key = AlbumIconPixmapCacheKey(idx);
|
||||
|
||||
QPixmap cached_pixmap;
|
||||
if (QPixmapCache::find(cache_key, &cached_pixmap)) {
|
||||
@@ -883,8 +919,8 @@ QVariant CollectionModel::AlbumIcon(CollectionItem *item) {
|
||||
}
|
||||
|
||||
// Try to load it from the disk cache
|
||||
if (use_disk_cache_ && icon_disk_cache_) {
|
||||
ScopedPtr<QIODevice> disk_cache_img(icon_disk_cache_->data(AlbumIconPixmapDiskCacheKey(cache_key)));
|
||||
if (use_disk_cache_ && sIconCache) {
|
||||
ScopedPtr<QIODevice> disk_cache_img(sIconCache->data(AlbumIconPixmapDiskCacheKey(cache_key)));
|
||||
if (disk_cache_img) {
|
||||
QImage cached_image;
|
||||
if (cached_image.load(&*disk_cache_img, "XPM")) {
|
||||
@@ -900,12 +936,12 @@ QVariant CollectionModel::AlbumIcon(CollectionItem *item) {
|
||||
}
|
||||
|
||||
// No art is cached and we're not loading it already. Load art for the first song in the album.
|
||||
const SongList songs = GetChildSongs(item);
|
||||
SongList songs = GetChildSongs(idx);
|
||||
if (!songs.isEmpty()) {
|
||||
AlbumCoverLoaderOptions cover_loader_options(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage);
|
||||
cover_loader_options.desired_scaled_size = QSize(kPrettyCoverSize, kPrettyCoverSize);
|
||||
cover_loader_options.types = cover_types_;
|
||||
const quint64 id = albumcover_loader_->LoadImageAsync(cover_loader_options, songs.first());
|
||||
const quint64 id = app_->album_cover_loader()->LoadImageAsync(cover_loader_options, songs.first());
|
||||
pending_art_[id] = ItemAndCacheKey(item, cache_key);
|
||||
pending_cache_keys_.insert(cache_key);
|
||||
}
|
||||
@@ -938,19 +974,19 @@ void CollectionModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderR
|
||||
}
|
||||
|
||||
// If we have a valid cover not already in the disk cache
|
||||
if (use_disk_cache_ && icon_disk_cache_ && result.success && !result.image_scaled.isNull()) {
|
||||
if (use_disk_cache_ && sIconCache && result.success && !result.image_scaled.isNull()) {
|
||||
const QUrl disk_cache_key = AlbumIconPixmapDiskCacheKey(cache_key);
|
||||
ScopedPtr<QIODevice> disk_cache_img(icon_disk_cache_->data(disk_cache_key));
|
||||
ScopedPtr<QIODevice> disk_cache_img(sIconCache->data(disk_cache_key));
|
||||
if (!disk_cache_img) {
|
||||
QNetworkCacheMetaData disk_cache_metadata;
|
||||
disk_cache_metadata.setSaveToDisk(true);
|
||||
disk_cache_metadata.setUrl(disk_cache_key);
|
||||
// Qt 6 now ignores any entry without headers, so add a fake header.
|
||||
disk_cache_metadata.setRawHeaders(QNetworkCacheMetaData::RawHeaderList() << qMakePair(QByteArray("collection-thumbnail"), cache_key.toUtf8()));
|
||||
QIODevice *device_iconcache = icon_disk_cache_->prepare(disk_cache_metadata);
|
||||
QIODevice *device_iconcache = sIconCache->prepare(disk_cache_metadata);
|
||||
if (device_iconcache) {
|
||||
result.image_scaled.save(device_iconcache, "XPM");
|
||||
icon_disk_cache_->insert(device_iconcache);
|
||||
sIconCache->insert(device_iconcache);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1024,14 +1060,14 @@ QString CollectionModel::TextOrUnknown(const QString &text) {
|
||||
QString CollectionModel::PrettyYearAlbum(const int year, const QString &album) {
|
||||
|
||||
if (year <= 0) return TextOrUnknown(album);
|
||||
return QString::number(year) + " - "_L1 + TextOrUnknown(album);
|
||||
return QString::number(year) + QLatin1String(" - ") + TextOrUnknown(album);
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::PrettyAlbumDisc(const QString &album, const int disc) {
|
||||
|
||||
if (disc <= 0 || Song::AlbumContainsDisc(album)) return TextOrUnknown(album);
|
||||
return TextOrUnknown(album) + " - (Disc "_L1 + QString::number(disc) + ")"_L1;
|
||||
return TextOrUnknown(album) + QLatin1String(" - (Disc ") + QString::number(disc) + QLatin1String(")");
|
||||
|
||||
}
|
||||
|
||||
@@ -1040,9 +1076,9 @@ QString CollectionModel::PrettyYearAlbumDisc(const int year, const QString &albu
|
||||
QString str;
|
||||
|
||||
if (year <= 0) str = TextOrUnknown(album);
|
||||
else str = QString::number(year) + " - "_L1 + TextOrUnknown(album);
|
||||
else str = QString::number(year) + QLatin1String(" - ") + TextOrUnknown(album);
|
||||
|
||||
if (!Song::AlbumContainsDisc(album) && disc > 0) str += " - (Disc "_L1 + QString::number(disc) + ")"_L1;
|
||||
if (!Song::AlbumContainsDisc(album) && disc > 0) str += QLatin1String(" - (Disc ") + QString::number(disc) + QLatin1String(")");
|
||||
|
||||
return str;
|
||||
|
||||
@@ -1050,7 +1086,7 @@ QString CollectionModel::PrettyYearAlbumDisc(const int year, const QString &albu
|
||||
|
||||
QString CollectionModel::PrettyDisc(const int disc) {
|
||||
|
||||
return "Disc "_L1 + QString::number(std::max(1, disc));
|
||||
return QLatin1String("Disc ") + QString::number(std::max(1, disc));
|
||||
|
||||
}
|
||||
|
||||
@@ -1068,7 +1104,7 @@ QString CollectionModel::PrettyFormat(const Song &song) {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles) {
|
||||
QString CollectionModel::SortText(const GroupBy group_by, const int container_level, const Song &song, const bool sort_skips_articles) {
|
||||
|
||||
switch (group_by) {
|
||||
case GroupBy::AlbumArtist:
|
||||
@@ -1076,7 +1112,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 SortText(song.album());
|
||||
return SortTextForArtist(song.album(), sort_skips_articles);
|
||||
case GroupBy::AlbumDisc:
|
||||
return song.album() + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::YearAlbum:
|
||||
@@ -1112,8 +1148,12 @@ QString CollectionModel::SortText(const GroupBy group_by, const Song &song, cons
|
||||
case GroupBy::Bitrate:
|
||||
return SortTextForNumber(std::max(0, song.bitrate())) + QLatin1Char(' ');
|
||||
case GroupBy::None:
|
||||
case GroupBy::GroupByCount:
|
||||
break;
|
||||
case GroupBy::GroupByCount:{
|
||||
if (container_level == 1 && !IsAlbumGroupBy(options_active_.group_by[0])) {
|
||||
return SortText(song.title());
|
||||
}
|
||||
return SortTextForSong(song);
|
||||
}
|
||||
}
|
||||
|
||||
return QString();
|
||||
@@ -1123,12 +1163,12 @@ QString CollectionModel::SortText(const GroupBy group_by, const Song &song, cons
|
||||
QString CollectionModel::SortText(QString text) {
|
||||
|
||||
if (text.isEmpty()) {
|
||||
text = " unknown"_L1;
|
||||
text = QLatin1String(" unknown");
|
||||
}
|
||||
else {
|
||||
text = text.toLower();
|
||||
}
|
||||
static const QRegularExpression regex_not_words(u"[^\\w ]"_s, QRegularExpression::UseUnicodePropertiesOption);
|
||||
static const QRegularExpression regex_not_words(QStringLiteral("[^\\w ]"), QRegularExpression::UseUnicodePropertiesOption);
|
||||
text = text.remove(regex_not_words);
|
||||
|
||||
return text;
|
||||
@@ -1143,7 +1183,7 @@ QString CollectionModel::SortTextForArtist(QString artist, const bool skip_artic
|
||||
for (const auto &i : Song::kArticles) {
|
||||
if (artist.startsWith(i)) {
|
||||
qint64 ilen = i.length();
|
||||
artist = artist.right(artist.length() - ilen) + ", "_L1 + i.left(ilen - 1);
|
||||
artist = artist.right(artist.length() - ilen) + QLatin1String(", ") + i.left(ilen - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1154,6 +1194,7 @@ QString CollectionModel::SortTextForArtist(QString artist, const bool skip_artic
|
||||
}
|
||||
|
||||
QString CollectionModel::SortTextForNumber(const int number) {
|
||||
|
||||
return QStringLiteral("%1").arg(number, 4, 10, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
@@ -1184,7 +1225,6 @@ bool CollectionModel::IsSongTitleDataChanged(const Song &song1, const Song &song
|
||||
|
||||
return song1.url() != song2.url() ||
|
||||
song1.track() != song2.track() ||
|
||||
song1.disc() != song2.disc() ||
|
||||
song1.title() != song2.title() ||
|
||||
song1.compilation() != song2.compilation() ||
|
||||
(song1.compilation() && song1.artist() != song2.artist());
|
||||
@@ -1282,7 +1322,7 @@ QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song,
|
||||
|
||||
// Make sure we distinguish albums by different artists if the parent group by is not including artist.
|
||||
if (IsAlbumGroupBy(group_by) && !has_unique_album_identifier && !song.is_compilation() && !song.effective_albumartist().isEmpty()) {
|
||||
key.prepend(u'-');
|
||||
key.prepend(QLatin1Char('-'));
|
||||
key.prepend(TextOrUnknown(song.effective_albumartist()));
|
||||
has_unique_album_identifier = true;
|
||||
}
|
||||
@@ -1311,8 +1351,8 @@ QString CollectionModel::DividerKey(const GroupBy group_by, const Song &song, co
|
||||
case GroupBy::Format:
|
||||
case GroupBy::FileType: {
|
||||
QChar c = sort_text[0];
|
||||
if (c.isDigit()) return u"0"_s;
|
||||
if (c == u' ') return QString();
|
||||
if (c.isDigit()) return QStringLiteral("0");
|
||||
if (c == QLatin1Char(' ')) return QString();
|
||||
if (c.decompositionTag() != QChar::NoDecomposition) {
|
||||
QString decomposition = c.decomposition();
|
||||
return QChar(decomposition[0]);
|
||||
@@ -1361,25 +1401,25 @@ QString CollectionModel::DividerDisplayText(const GroupBy group_by, const QStrin
|
||||
case GroupBy::Genre:
|
||||
case GroupBy::FileType:
|
||||
case GroupBy::Format:
|
||||
if (key == "0"_L1) return u"0-9"_s;
|
||||
if (key == QLatin1String("0")) return QStringLiteral("0-9");
|
||||
return key.toUpper();
|
||||
|
||||
case GroupBy::YearAlbum:
|
||||
case GroupBy::YearAlbumDisc:
|
||||
case GroupBy::OriginalYearAlbum:
|
||||
case GroupBy::OriginalYearAlbumDisc:
|
||||
if (key == "0000"_L1) return tr("Unknown");
|
||||
if (key == QLatin1String("0000")) return tr("Unknown");
|
||||
return key.toUpper();
|
||||
|
||||
case GroupBy::Year:
|
||||
case GroupBy::OriginalYear:
|
||||
if (key == "0000"_L1) return tr("Unknown");
|
||||
if (key == QLatin1String("0000")) return tr("Unknown");
|
||||
return QString::number(key.toInt()); // To remove leading 0s
|
||||
|
||||
case GroupBy::Samplerate:
|
||||
case GroupBy::Bitdepth:
|
||||
case GroupBy::Bitrate:
|
||||
if (key == "000"_L1) return tr("Unknown");
|
||||
if (key == QLatin1String("000")) return tr("Unknown");
|
||||
return QString::number(key.toInt()); // To remove leading 0s
|
||||
|
||||
case GroupBy::None:
|
||||
@@ -1393,37 +1433,26 @@ QString CollectionModel::DividerDisplayText(const GroupBy group_by, const QStrin
|
||||
|
||||
}
|
||||
|
||||
bool CollectionModel::CompareItems(CollectionItem *a, CollectionItem *b) const {
|
||||
bool CollectionModel::CompareItems(const CollectionItem *a, const CollectionItem *b) const {
|
||||
|
||||
QVariant left = data(a, CollectionModel::Role_SortText);
|
||||
QVariant right = data(b, CollectionModel::Role_SortText);
|
||||
QVariant left(data(a, CollectionModel::Role_SortText));
|
||||
QVariant right(data(b, CollectionModel::Role_SortText));
|
||||
|
||||
if (left.metaType().id() == QMetaType::Int) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
if (left.metaType().id() == QMetaType::Int)
|
||||
#else
|
||||
if (left.type() == QVariant::Int)
|
||||
#endif
|
||||
return left.toInt() < right.toInt();
|
||||
}
|
||||
else {
|
||||
else
|
||||
return left.toString() < right.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CollectionModel::HasParentAlbumGroupBy(CollectionItem *item) const {
|
||||
|
||||
while (item && item != root_) {
|
||||
if (item->container_level >= 0 && item->container_level <= 2 && IsAlbumGroupBy(options_active_.group_by[item->container_level])) {
|
||||
return true;
|
||||
}
|
||||
item = item->parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
qint64 CollectionModel::MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default) {
|
||||
|
||||
qint64 size = s->value(size_id, cache_size_default).toInt();
|
||||
int unit = s->value(size_unit_id, static_cast<int>(CollectionSettings::CacheSizeUnit::MB)).toInt() + 1;
|
||||
int unit = s->value(size_unit_id, static_cast<int>(CollectionSettingsPage::CacheSizeUnit::MB)).toInt() + 1;
|
||||
|
||||
do {
|
||||
size *= 1024;
|
||||
@@ -1434,23 +1463,24 @@ qint64 CollectionModel::MaximumCacheSize(Settings *s, const char *size_id, const
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::GetChildSongs(CollectionItem *item, SongList &songs, QSet<int> &song_ids, QList<QUrl> &urls) const {
|
||||
void CollectionModel::GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) 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, songs, song_ids, urls);
|
||||
GetChildSongs(child, urls, songs, song_ids);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CollectionItem::Type::Song:
|
||||
urls << item->metadata.url();
|
||||
if (!song_ids.contains(item->metadata.id())) {
|
||||
songs << item->metadata;
|
||||
song_ids << item->metadata.id();
|
||||
urls->append(item->metadata.url());
|
||||
if (!song_ids->contains(item->metadata.id())) {
|
||||
songs->append(item->metadata);
|
||||
song_ids->insert(item->metadata.id());
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1460,33 +1490,16 @@ void CollectionModel::GetChildSongs(CollectionItem *item, SongList &songs, QSet<
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
SongList songs;
|
||||
QList<QUrl> dontcare;
|
||||
SongList ret;
|
||||
QSet<int> song_ids;
|
||||
QList<QUrl> urls;
|
||||
for (const QModelIndex &idx : indexes) {
|
||||
GetChildSongs(IndexToItem(idx), songs, song_ids, urls);
|
||||
}
|
||||
|
||||
return songs;
|
||||
for (const QModelIndex &idx : indexes) {
|
||||
GetChildSongs(IndexToItem(idx), &dontcare, &ret, &song_ids);
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
@@ -1542,11 +1555,8 @@ void CollectionModel::TotalAlbumCountUpdatedSlot(const int count) {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ClearIconDiskCache() {
|
||||
|
||||
if (icon_disk_cache_) icon_disk_cache_->clear();
|
||||
QPixmapCache::clear();
|
||||
|
||||
void CollectionModel::ClearDiskCache() {
|
||||
if (sIconCache) sIconCache->clear();
|
||||
}
|
||||
|
||||
void CollectionModel::RowsInserted(const QModelIndex &parent, const int first, const int last) {
|
||||
|
||||
@@ -44,9 +44,10 @@
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QQueue>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/simpletreemodel.h"
|
||||
#include "core/song.h"
|
||||
#include "core/sqlrow.h"
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
#include "collectionmodelupdate.h"
|
||||
@@ -56,16 +57,16 @@
|
||||
class QTimer;
|
||||
class Settings;
|
||||
|
||||
class Application;
|
||||
class CollectionBackend;
|
||||
class CollectionDirectoryModel;
|
||||
class CollectionFilter;
|
||||
class AlbumCoverLoader;
|
||||
|
||||
class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CollectionModel(const SharedPtr<CollectionBackend> backend, const SharedPtr<AlbumCoverLoader> albumcover_loader, QObject *parent = nullptr);
|
||||
explicit CollectionModel(SharedPtr<CollectionBackend> backend, Application *app, QObject *parent = nullptr);
|
||||
~CollectionModel() override;
|
||||
|
||||
static const int kPrettyCoverSize;
|
||||
@@ -155,7 +156,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
int total_artist_count() const { return total_artist_count_; }
|
||||
int total_album_count() const { return total_album_count_; }
|
||||
|
||||
quint64 icon_disk_cache_size() { return icon_disk_cache_->cacheSize(); }
|
||||
quint64 icon_cache_disk_size() { return sIconCache->cacheSize(); }
|
||||
|
||||
const CollectionModel::Grouping GetGroupBy() const { return options_current_.group_by; }
|
||||
void SetGroupBy(const CollectionModel::Grouping g, const std::optional<bool> separate_albums_by_grouping = std::optional<bool>());
|
||||
@@ -183,7 +184,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
||||
static QString PrettyDisc(const int disc);
|
||||
static QString PrettyFormat(const Song &song);
|
||||
QString SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles);
|
||||
QString SortText(const GroupBy group_by, const int container_level, const Song &song, const bool sort_skips_articles);
|
||||
static QString SortText(QString text);
|
||||
static QString SortTextForNumber(const int number);
|
||||
static QString SortTextForArtist(QString artist, const bool skip_articles);
|
||||
@@ -194,15 +195,11 @@ 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, SongList &songs, QSet<int> &song_ids, QList<QUrl> &urls) const;
|
||||
SongList GetChildSongs(const QList<CollectionItem*> items) const;
|
||||
SongList GetChildSongs(CollectionItem *item) const;
|
||||
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
|
||||
SongList GetChildSongs(const QModelIndex &idx) const;
|
||||
SongList GetChildSongs(const QModelIndexList &indexes) const;
|
||||
|
||||
bool CompareItems(CollectionItem *a, CollectionItem *b) const;
|
||||
|
||||
bool HasParentAlbumGroupBy(CollectionItem *item) const;
|
||||
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void TotalSongCountUpdated(const int count);
|
||||
@@ -219,14 +216,12 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
void AddReAddOrUpdate(const SongList &songs);
|
||||
void RemoveSongs(const SongList &songs);
|
||||
|
||||
void ClearIconDiskCache();
|
||||
|
||||
private:
|
||||
void Clear();
|
||||
void BeginReset();
|
||||
void EndReset();
|
||||
|
||||
QVariant data(CollectionItem *item, const int role) const;
|
||||
QVariant data(const CollectionItem *item, const int role) const;
|
||||
|
||||
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
|
||||
void ScheduleAddSongs(const SongList &songs);
|
||||
@@ -241,7 +236,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
void CreateDividerItem(const QString ÷r_key, const QString &display_text, CollectionItem *parent);
|
||||
CollectionItem *CreateContainerItem(const GroupBy group_by, const int container_level, const QString &container_key, const Song &song, CollectionItem *parent);
|
||||
void CreateSongItem(const Song &song, CollectionItem *parent);
|
||||
void SetSongItemData(CollectionItem *item, const Song &song) const;
|
||||
void SetSongItemData(CollectionItem *item, const Song &song);
|
||||
CollectionItem *CreateCompilationArtistNode(CollectionItem *parent);
|
||||
|
||||
void LoadSongsFromSqlAsync();
|
||||
@@ -252,9 +247,9 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
|
||||
// Helpers
|
||||
static bool IsCompilationArtistNode(const CollectionItem *node) { return node == node->parent->compilation_artist_node_; }
|
||||
QString AlbumIconPixmapCacheKey(const CollectionItem *item) const;
|
||||
QString AlbumIconPixmapCacheKey(const QModelIndex &idx) const;
|
||||
static QUrl AlbumIconPixmapDiskCacheKey(const QString &cache_key);
|
||||
QVariant AlbumIcon(CollectionItem *item);
|
||||
QVariant AlbumIcon(const QModelIndex &idx);
|
||||
void ClearItemPixmapCache(CollectionItem *item);
|
||||
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
|
||||
|
||||
@@ -270,12 +265,15 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
void TotalArtistCountUpdatedSlot(const int count);
|
||||
void TotalAlbumCountUpdatedSlot(const int count);
|
||||
|
||||
static void ClearDiskCache();
|
||||
|
||||
void RowsInserted(const QModelIndex &parent, const int first, const int last);
|
||||
void RowsRemoved(const QModelIndex &parent, const int first, const int last);
|
||||
|
||||
private:
|
||||
const SharedPtr<CollectionBackend> backend_;
|
||||
const SharedPtr<AlbumCoverLoader> albumcover_loader_;
|
||||
static QNetworkDiskCache *sIconCache;
|
||||
SharedPtr<CollectionBackend> backend_;
|
||||
Application *app_;
|
||||
CollectionDirectoryModel *dir_model_;
|
||||
CollectionFilter *filter_;
|
||||
QTimer *timer_reload_;
|
||||
@@ -310,8 +308,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
using ItemAndCacheKey = QPair<CollectionItem*, QString>;
|
||||
QMap<quint64, ItemAndCacheKey> pending_art_;
|
||||
QSet<QString> pending_cache_keys_;
|
||||
|
||||
QNetworkDiskCache *icon_disk_cache_;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(CollectionModel::Grouping)
|
||||
|
||||
@@ -24,9 +24,8 @@
|
||||
#include <QVariant>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "collectionplaylistitem.h"
|
||||
#include "tagreader/tagreaderclient.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
|
||||
class SqlRow;
|
||||
|
||||
@@ -42,9 +41,9 @@ QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
|
||||
|
||||
void CollectionPlaylistItem::Reload() {
|
||||
|
||||
const TagReaderResult result = TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
|
||||
const TagReaderClient::Result result = TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
|
||||
if (!result.success()) {
|
||||
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
|
||||
qLog(Error) << "Could not reload file" << song_.url() << result.error;
|
||||
return;
|
||||
}
|
||||
UpdateTemporaryMetadata(song_);
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QRegularExpression>
|
||||
#include <QSqlDatabase>
|
||||
|
||||
#include "core/sqlquery.h"
|
||||
@@ -37,8 +38,6 @@
|
||||
#include "collectionquery.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options)
|
||||
: SqlQuery(db),
|
||||
songs_table_(songs_table),
|
||||
@@ -49,14 +48,14 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
|
||||
if (filter_options.max_age() != -1) {
|
||||
qint64 cutoff = QDateTime::currentSecsSinceEpoch() - filter_options.max_age();
|
||||
|
||||
where_clauses_ << u"ctime > ?"_s;
|
||||
where_clauses_ << QStringLiteral("ctime > ?");
|
||||
bound_values_ << cutoff;
|
||||
}
|
||||
|
||||
duplicates_only_ = filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Duplicates;
|
||||
|
||||
if (filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Untagged) {
|
||||
where_clauses_ << u"(artist = '' OR album = '' OR title ='')"_s;
|
||||
where_clauses_ << QStringLiteral("(artist = '' OR album = '' OR title ='')");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -64,25 +63,35 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
|
||||
void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) {
|
||||
|
||||
// Ignore 'literal' for IN
|
||||
if (op.compare("IN"_L1, Qt::CaseInsensitive) == 0) {
|
||||
if (op.compare(QLatin1String("IN"), Qt::CaseInsensitive) == 0) {
|
||||
const QStringList values = value.toStringList();
|
||||
QStringList final_values;
|
||||
final_values.reserve(values.count());
|
||||
for (const QString &single_value : values) {
|
||||
final_values.append(u"?"_s);
|
||||
final_values.append(QStringLiteral("?"));
|
||||
bound_values_ << single_value;
|
||||
}
|
||||
|
||||
where_clauses_ << QStringLiteral("%1 IN (%2)").arg(column, final_values.join(u','));
|
||||
where_clauses_ << QStringLiteral("%1 IN (%2)").arg(column, final_values.join(QLatin1Char(',')));
|
||||
}
|
||||
else {
|
||||
// Do integers inline - sqlite seems to get confused when you pass integers to bound parameters
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
if (value.metaType().id() == QMetaType::Int) {
|
||||
#else
|
||||
if (value.type() == QVariant::Int) {
|
||||
#endif
|
||||
where_clauses_ << QStringLiteral("%1 %2 %3").arg(column, op, value.toString());
|
||||
}
|
||||
else if (value.metaType().id() == QMetaType::QString && value.toString().isNull()) {
|
||||
else if (
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
value.metaType().id() == QMetaType::QString
|
||||
#else
|
||||
value.type() == QVariant::String
|
||||
#endif
|
||||
&& value.toString().isNull()) {
|
||||
where_clauses_ << QStringLiteral("%1 %2 ?").arg(column, op);
|
||||
bound_values_ << ""_L1;
|
||||
bound_values_ << QLatin1String("");
|
||||
}
|
||||
else {
|
||||
where_clauses_ << QStringLiteral("%1 %2 ?").arg(column, op);
|
||||
@@ -113,16 +122,16 @@ bool CollectionQuery::Exec() {
|
||||
|
||||
QStringList where_clauses(where_clauses_);
|
||||
if (!include_unavailable_) {
|
||||
where_clauses << u"unavailable = 0"_s;
|
||||
where_clauses << QStringLiteral("unavailable = 0");
|
||||
}
|
||||
|
||||
if (!where_clauses.isEmpty()) sql += " WHERE "_L1 + where_clauses.join(" AND "_L1);
|
||||
if (!where_clauses.isEmpty()) sql += QLatin1String(" WHERE ") + where_clauses.join(QLatin1String(" AND "));
|
||||
|
||||
if (!order_by_.isEmpty()) sql += " ORDER BY "_L1 + order_by_;
|
||||
if (!order_by_.isEmpty()) sql += QLatin1String(" ORDER BY ") + order_by_;
|
||||
|
||||
if (limit_ != -1) sql += " LIMIT "_L1 + QString::number(limit_);
|
||||
if (limit_ != -1) sql += QLatin1String(" LIMIT ") + QString::number(limit_);
|
||||
|
||||
sql.replace("%songs_table"_L1, songs_table_);
|
||||
sql.replace(QLatin1String("%songs_table"), songs_table_);
|
||||
|
||||
if (!QSqlQuery::prepare(sql)) return false;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "collectiontask.h"
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#include <QtGlobal>
|
||||
#include <QString>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/shared_ptr.h"
|
||||
|
||||
class TaskManager;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QWidget>
|
||||
#include <QAbstractItemView>
|
||||
#include <QTreeView>
|
||||
#include <QItemSelectionModel>
|
||||
@@ -45,27 +46,24 @@
|
||||
#include <QAction>
|
||||
#include <QMessageBox>
|
||||
#include <QSettings>
|
||||
#include <QPaintEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QtEvents>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/mimedata.h"
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/deletefiles.h"
|
||||
#include "core/settings.h"
|
||||
#include "utilities/filemanagerutils.h"
|
||||
#include "collectionlibrary.h"
|
||||
#include "collection.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "collectiondirectorymodel.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionfilter.h"
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "collectionitem.h"
|
||||
#include "collectionitemdelegate.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionview.h"
|
||||
#ifndef Q_OS_WIN32
|
||||
#ifndef Q_OS_WIN
|
||||
# include "device/devicemanager.h"
|
||||
# include "device/devicestatefiltermodel.h"
|
||||
#endif
|
||||
@@ -73,20 +71,18 @@
|
||||
#include "dialogs/deleteconfirmationdialog.h"
|
||||
#include "organize/organizedialog.h"
|
||||
#include "organize/organizeerrordialog.h"
|
||||
#include "constants/collectionsettings.h"
|
||||
#include "settings/collectionsettingspage.h"
|
||||
|
||||
using std::make_unique;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
CollectionView::CollectionView(QWidget *parent)
|
||||
: AutoExpandingTreeView(parent),
|
||||
model_(nullptr),
|
||||
app_(nullptr),
|
||||
filter_(nullptr),
|
||||
filter_widget_(nullptr),
|
||||
total_song_count_(-1),
|
||||
total_artist_count_(-1),
|
||||
total_album_count_(-1),
|
||||
nomusic_(u":/pictures/nomusic.png"_s),
|
||||
nomusic_(QStringLiteral(":/pictures/nomusic.png")),
|
||||
context_menu_(nullptr),
|
||||
action_load_(nullptr),
|
||||
action_add_to_playlist_(nullptr),
|
||||
@@ -95,7 +91,7 @@ CollectionView::CollectionView(QWidget *parent)
|
||||
action_open_in_new_playlist_(nullptr),
|
||||
action_organize_(nullptr),
|
||||
action_search_for_this_(nullptr),
|
||||
#ifndef Q_OS_WIN32
|
||||
#ifndef Q_OS_WIN
|
||||
action_copy_to_device_(nullptr),
|
||||
#endif
|
||||
action_edit_track_(nullptr),
|
||||
@@ -108,7 +104,7 @@ CollectionView::CollectionView(QWidget *parent)
|
||||
is_in_keyboard_search_(false),
|
||||
delete_files_(false) {
|
||||
|
||||
setObjectName(QLatin1String(QObject::metaObject()->className()));
|
||||
setObjectName(QLatin1String(metaObject()->className()));
|
||||
|
||||
setItemDelegate(new CollectionItemDelegate(this));
|
||||
setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
@@ -118,41 +114,12 @@ CollectionView::CollectionView(QWidget *parent)
|
||||
setDragDropMode(QAbstractItemView::DragOnly);
|
||||
setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
|
||||
setStyleSheet(u"QTreeView::item{padding-top:1px;}"_s);
|
||||
setStyleSheet(QStringLiteral("QTreeView::item{padding-top:1px;}"));
|
||||
|
||||
}
|
||||
|
||||
CollectionView::~CollectionView() = default;
|
||||
|
||||
void CollectionView::Init(const SharedPtr<TaskManager> task_manager,
|
||||
const SharedPtr<TagReaderClient> tagreader_client,
|
||||
const SharedPtr<NetworkAccessManager> network,
|
||||
const SharedPtr<AlbumCoverLoader> albumcover_loader,
|
||||
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader,
|
||||
const SharedPtr<CoverProviders> cover_providers,
|
||||
const SharedPtr<LyricsProviders> lyrics_providers,
|
||||
const SharedPtr<CollectionLibrary> collection,
|
||||
const SharedPtr<DeviceManager> device_manager,
|
||||
const SharedPtr<StreamingServices> streaming_services) {
|
||||
|
||||
task_manager_ = task_manager;
|
||||
tagreader_client_ = tagreader_client;
|
||||
network_ = network;
|
||||
albumcover_loader_ = albumcover_loader;
|
||||
current_albumcover_loader_ = current_albumcover_loader;
|
||||
cover_providers_ = cover_providers;
|
||||
lyrics_providers_ = lyrics_providers;
|
||||
collection_ = collection;
|
||||
device_manager_ = device_manager;
|
||||
streaming_services_ = streaming_services;
|
||||
backend_ = collection_->backend();
|
||||
model_ = collection_->model();
|
||||
filter_ = collection_->model()->filter();
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::SaveFocus() {
|
||||
|
||||
const QModelIndex current = currentIndex();
|
||||
@@ -172,8 +139,8 @@ void CollectionView::SaveFocus() {
|
||||
|
||||
switch (item_type) {
|
||||
case CollectionItem::Type::Song:{
|
||||
QModelIndex index = filter_->mapToSource(current);
|
||||
SongList songs = model_->GetChildSongs(index);
|
||||
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
SongList songs = app_->collection_model()->GetChildSongs(index);
|
||||
if (!songs.isEmpty()) {
|
||||
last_selected_song_ = songs.last();
|
||||
}
|
||||
@@ -240,8 +207,8 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
|
||||
break;
|
||||
case CollectionItem::Type::Song:
|
||||
if (!last_selected_song_.url().isEmpty()) {
|
||||
QModelIndex index = filter_->mapToSource(current);
|
||||
const SongList songs = model_->GetChildSongs(index);
|
||||
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
const SongList songs = app_->collection_model()->GetChildSongs(index);
|
||||
if (std::any_of(songs.begin(), songs.end(), [this](const Song &song) { return song == last_selected_song_; })) {
|
||||
setCurrentIndex(current);
|
||||
return true;
|
||||
@@ -271,7 +238,6 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
@@ -279,14 +245,22 @@ bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
|
||||
void CollectionView::ReloadSettings() {
|
||||
|
||||
Settings settings;
|
||||
settings.beginGroup(CollectionSettings::kSettingsGroup);
|
||||
SetAutoOpen(settings.value(CollectionSettings::kAutoOpen, false).toBool());
|
||||
delete_files_ = settings.value(CollectionSettings::kDeleteFiles, false).toBool();
|
||||
settings.beginGroup(CollectionSettingsPage::kSettingsGroup);
|
||||
SetAutoOpen(settings.value("auto_open", false).toBool());
|
||||
delete_files_ = settings.value("delete_files", false).toBool();
|
||||
settings.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::SetFilterWidget(CollectionFilterWidget *filter_widget) { filter_widget_ = filter_widget; }
|
||||
void CollectionView::SetApplication(Application *app) {
|
||||
|
||||
app_ = app;
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::SetFilter(CollectionFilterWidget *filter) { filter_ = filter; }
|
||||
|
||||
void CollectionView::TotalSongCountUpdated(const int count) {
|
||||
|
||||
@@ -376,7 +350,7 @@ void CollectionView::mouseReleaseEvent(QMouseEvent *e) {
|
||||
QTreeView::mouseReleaseEvent(e);
|
||||
|
||||
if (total_song_count_ == 0) {
|
||||
Q_EMIT ShowSettingsDialog();
|
||||
Q_EMIT ShowConfigDialog();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -403,29 +377,29 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
if (!context_menu_) {
|
||||
context_menu_ = new QMenu(this);
|
||||
action_add_to_playlist_ = context_menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Append to current playlist"), this, &CollectionView::AddToPlaylist);
|
||||
action_load_ = context_menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Replace current playlist"), this, &CollectionView::Load);
|
||||
action_open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load(u"document-new"_s), tr("Open in new playlist"), this, &CollectionView::OpenInNewPlaylist);
|
||||
action_add_to_playlist_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("media-playback-start")), tr("Append to current playlist"), this, &CollectionView::AddToPlaylist);
|
||||
action_load_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("media-playback-start")), tr("Replace current playlist"), this, &CollectionView::Load);
|
||||
action_open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("document-new")), tr("Open in new playlist"), this, &CollectionView::OpenInNewPlaylist);
|
||||
|
||||
context_menu_->addSeparator();
|
||||
action_add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load(u"go-next"_s), tr("Queue track"), this, &CollectionView::AddToPlaylistEnqueue);
|
||||
action_add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load(u"go-next"_s), tr("Queue to play next"), this, &CollectionView::AddToPlaylistEnqueueNext);
|
||||
action_add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("go-next")), tr("Queue track"), this, &CollectionView::AddToPlaylistEnqueue);
|
||||
action_add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("go-next")), tr("Queue to play next"), this, &CollectionView::AddToPlaylistEnqueueNext);
|
||||
|
||||
context_menu_->addSeparator();
|
||||
|
||||
action_search_for_this_ = context_menu_->addAction(IconLoader::Load(u"edit-find"_s), tr("Search for this"), this, &CollectionView::SearchForThis);
|
||||
action_search_for_this_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-find")), tr("Search for this"), this, &CollectionView::SearchForThis);
|
||||
|
||||
context_menu_->addSeparator();
|
||||
action_organize_ = context_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &CollectionView::Organize);
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &CollectionView::CopyToDevice);
|
||||
action_organize_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-copy")), tr("Organize files..."), this, &CollectionView::Organize);
|
||||
#ifndef Q_OS_WIN
|
||||
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("device")), tr("Copy to device..."), this, &CollectionView::CopyToDevice);
|
||||
#endif
|
||||
action_delete_files_ = context_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &CollectionView::Delete);
|
||||
action_delete_files_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-delete")), tr("Delete from disk..."), this, &CollectionView::Delete);
|
||||
|
||||
context_menu_->addSeparator();
|
||||
action_edit_track_ = context_menu_->addAction(IconLoader::Load(u"edit-rename"_s), tr("Edit track information..."), this, &CollectionView::EditTracks);
|
||||
action_edit_tracks_ = context_menu_->addAction(IconLoader::Load(u"edit-rename"_s), tr("Edit tracks information..."), this, &CollectionView::EditTracks);
|
||||
action_show_in_browser_ = context_menu_->addAction(IconLoader::Load(u"document-open-folder"_s), tr("Show in file browser..."), this, &CollectionView::ShowInBrowser);
|
||||
action_edit_track_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-rename")), tr("Edit track information..."), this, &CollectionView::EditTracks);
|
||||
action_edit_tracks_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("edit-rename")), tr("Edit tracks information..."), this, &CollectionView::EditTracks);
|
||||
action_show_in_browser_ = context_menu_->addAction(IconLoader::Load(QStringLiteral("document-open-folder")), tr("Show in file browser..."), this, &CollectionView::ShowInBrowser);
|
||||
|
||||
context_menu_->addSeparator();
|
||||
|
||||
@@ -437,11 +411,11 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
context_menu_->addSeparator();
|
||||
|
||||
context_menu_->addMenu(filter_widget_->menu());
|
||||
context_menu_->addMenu(filter_->menu());
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
action_copy_to_device_->setDisabled(device_manager_->connected_devices_model()->rowCount() == 0);
|
||||
QObject::connect(device_manager_->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, action_copy_to_device_, &QAction::setDisabled);
|
||||
#ifndef Q_OS_WIN
|
||||
action_copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
|
||||
QObject::connect(app_->device_manager()->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, action_copy_to_device_, &QAction::setDisabled);
|
||||
#endif
|
||||
|
||||
}
|
||||
@@ -449,16 +423,16 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
context_menu_index_ = indexAt(e->pos());
|
||||
if (!context_menu_index_.isValid()) return;
|
||||
|
||||
context_menu_index_ = filter_->mapToSource(context_menu_index_);
|
||||
context_menu_index_ = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(context_menu_index_);
|
||||
|
||||
const QModelIndexList selected_indexes = filter_->mapSelectionToSource(selectionModel()->selection()).indexes();
|
||||
const QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
|
||||
|
||||
int regular_elements = 0;
|
||||
int regular_editable = 0;
|
||||
|
||||
for (const QModelIndex &idx : selected_indexes) {
|
||||
++regular_elements;
|
||||
if (model_->data(idx, CollectionModel::Role_Editable).toBool()) {
|
||||
if (app_->collection_model()->data(idx, CollectionModel::Role_Editable).toBool()) {
|
||||
++regular_editable;
|
||||
}
|
||||
}
|
||||
@@ -481,22 +455,30 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
action_rescan_songs_->setEnabled(regular_editable > 0);
|
||||
|
||||
action_organize_->setVisible(regular_elements == regular_editable);
|
||||
#ifndef Q_OS_WIN32
|
||||
#ifndef Q_OS_WIN
|
||||
action_copy_to_device_->setVisible(regular_elements == regular_editable);
|
||||
#endif
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
action_delete_files_->setVisible(delete_files_);
|
||||
#else
|
||||
action_delete_files_->setVisible(false);
|
||||
#endif
|
||||
|
||||
action_show_in_various_->setVisible(songs_selected > 0);
|
||||
action_no_show_in_various_->setVisible(songs_selected > 0);
|
||||
|
||||
// only when all selected items are editable
|
||||
action_organize_->setEnabled(regular_elements == regular_editable);
|
||||
#ifndef Q_OS_WIN32
|
||||
#ifndef Q_OS_WIN
|
||||
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
|
||||
#endif
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
action_delete_files_->setEnabled(delete_files_);
|
||||
#else
|
||||
action_delete_files_->setEnabled(false);
|
||||
#endif
|
||||
|
||||
context_menu_->popup(e->globalPos());
|
||||
|
||||
@@ -525,7 +507,7 @@ void CollectionView::SetShowInVarious(const bool on) {
|
||||
if (on && albums.keys().count() == 1) {
|
||||
const QStringList albums_list = albums.keys();
|
||||
const QString album = albums_list.first();
|
||||
const SongList all_of_album = backend_->GetSongsByAlbum(album);
|
||||
const SongList all_of_album = app_->collection_backend()->GetSongsByAlbum(album);
|
||||
QSet<QString> other_artists;
|
||||
for (const Song &s : all_of_album) {
|
||||
if (!albums.contains(album, s.artist()) && !other_artists.contains(s.artist())) {
|
||||
@@ -541,9 +523,13 @@ void CollectionView::SetShowInVarious(const bool on) {
|
||||
}
|
||||
}
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||
const QSet<QString> albums_set = QSet<QString>(albums.keyBegin(), albums.keyEnd());
|
||||
#else
|
||||
const QSet<QString> albums_set = QSet<QString>::fromList(albums.keys());
|
||||
#endif
|
||||
for (const QString &album : albums_set) {
|
||||
backend_->ForceCompilation(album, albums.values(album), on);
|
||||
app_->collection_backend()->ForceCompilation(album, albums.values(album), on);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -607,12 +593,11 @@ void CollectionView::SearchForThis() {
|
||||
return;
|
||||
}
|
||||
QString search;
|
||||
|
||||
QModelIndex index = filter_->mapToSource(current);
|
||||
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
|
||||
switch (item_type) {
|
||||
case CollectionItem::Type::Song:{
|
||||
SongList songs = model_->GetChildSongs(index);
|
||||
SongList songs = app_->collection_model()->GetChildSongs(index);
|
||||
if (!songs.isEmpty()) {
|
||||
last_selected_song_ = songs.last();
|
||||
}
|
||||
@@ -625,8 +610,8 @@ void CollectionView::SearchForThis() {
|
||||
}
|
||||
|
||||
case CollectionItem::Type::Container:{
|
||||
CollectionItem *item = model_->IndexToItem(index);
|
||||
const CollectionModel::GroupBy group_by = model_->GetGroupBy()[item->container_level];
|
||||
CollectionItem *item = app_->collection_model()->IndexToItem(index);
|
||||
const CollectionModel::GroupBy group_by = app_->collection_model()->GetGroupBy()[item->container_level];
|
||||
while (!item->children.isEmpty()) {
|
||||
item = item->children.constFirst();
|
||||
}
|
||||
@@ -687,7 +672,7 @@ void CollectionView::SearchForThis() {
|
||||
return;
|
||||
}
|
||||
|
||||
filter_widget_->ShowInCollection(search);
|
||||
filter_->ShowInCollection(search);
|
||||
|
||||
}
|
||||
|
||||
@@ -712,18 +697,18 @@ void CollectionView::scrollTo(const QModelIndex &idx, ScrollHint hint) {
|
||||
|
||||
SongList CollectionView::GetSelectedSongs() const {
|
||||
|
||||
QModelIndexList selected_indexes = filter_->mapSelectionToSource(selectionModel()->selection()).indexes();
|
||||
return model_->GetChildSongs(selected_indexes);
|
||||
QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
|
||||
return app_->collection_model()->GetChildSongs(selected_indexes);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::Organize() {
|
||||
|
||||
if (!organize_dialog_) {
|
||||
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, backend_, this);
|
||||
organize_dialog_ = make_unique<OrganizeDialog>(app_->task_manager(), app_->collection_backend(), this);
|
||||
}
|
||||
|
||||
organize_dialog_->SetDestinationModel(model_->directory_model());
|
||||
organize_dialog_->SetDestinationModel(app_->collection_model()->directory_model());
|
||||
organize_dialog_->SetCopy(false);
|
||||
const SongList songs = GetSelectedSongs();
|
||||
if (organize_dialog_->SetSongs(songs)) {
|
||||
@@ -738,7 +723,7 @@ void CollectionView::Organize() {
|
||||
void CollectionView::EditTracks() {
|
||||
|
||||
if (!edit_tag_dialog_) {
|
||||
edit_tag_dialog_ = make_unique<EditTagDialog>(network_, tagreader_client_, backend_, albumcover_loader_, current_albumcover_loader_, cover_providers_, lyrics_providers_, streaming_services_, this);
|
||||
edit_tag_dialog_ = make_unique<EditTagDialog>(app_, this);
|
||||
QObject::connect(&*edit_tag_dialog_, &EditTagDialog::Error, this, &CollectionView::EditTagError);
|
||||
}
|
||||
const SongList songs = GetSelectedSongs();
|
||||
@@ -753,18 +738,18 @@ void CollectionView::EditTagError(const QString &message) {
|
||||
|
||||
void CollectionView::RescanSongs() {
|
||||
|
||||
collection_->Rescan(GetSelectedSongs());
|
||||
app_->collection()->Rescan(GetSelectedSongs());
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::CopyToDevice() {
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
#ifndef Q_OS_WIN
|
||||
if (!organize_dialog_) {
|
||||
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
|
||||
organize_dialog_ = make_unique<OrganizeDialog>(app_->task_manager(), nullptr, this);
|
||||
}
|
||||
|
||||
organize_dialog_->SetDestinationModel(device_manager_->connected_devices_model(), true);
|
||||
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
|
||||
organize_dialog_->SetCopy(true);
|
||||
organize_dialog_->SetSongs(GetSelectedSongs());
|
||||
organize_dialog_->show();
|
||||
@@ -836,9 +821,9 @@ void CollectionView::Delete() {
|
||||
if (DeleteConfirmationDialog::warning(files) != QDialogButtonBox::Yes) return;
|
||||
|
||||
// We can cheat and always take the storage of the first directory, since they'll all be FilesystemMusicStorage in a collection and deleting doesn't check the actual directory.
|
||||
SharedPtr<MusicStorage> storage = model_->directory_model()->index(0, 0).data(MusicStorage::Role_Storage).value<SharedPtr<MusicStorage>>();
|
||||
SharedPtr<MusicStorage> storage = app_->collection_model()->directory_model()->index(0, 0).data(MusicStorage::Role_Storage).value<SharedPtr<MusicStorage>>();
|
||||
|
||||
DeleteFiles *delete_files = new DeleteFiles(task_manager_, storage, true);
|
||||
DeleteFiles *delete_files = new DeleteFiles(app_->task_manager(), storage, true);
|
||||
QObject::connect(delete_files, &DeleteFiles::Finished, this, &CollectionView::DeleteFilesFinished);
|
||||
delete_files->Start(songs);
|
||||
|
||||
|
||||
@@ -24,39 +24,26 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAbstractItemView>
|
||||
#include <QString>
|
||||
#include <QPixmap>
|
||||
#include <QSet>
|
||||
|
||||
#include "includes/scoped_ptr.h"
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/scoped_ptr.h"
|
||||
#include "core/song.h"
|
||||
#include "widgets/autoexpandingtreeview.h"
|
||||
|
||||
class QSortFilterProxyModel;
|
||||
class QWidget;
|
||||
class QMenu;
|
||||
class QAction;
|
||||
class QContextMenuEvent;
|
||||
class QMouseEvent;
|
||||
class QPaintEvent;
|
||||
class QKeyEvent;
|
||||
|
||||
class TaskManager;
|
||||
class TagReaderClient;
|
||||
class NetworkAccessManager;
|
||||
class CollectionLibrary;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
class CollectionFilter;
|
||||
class Application;
|
||||
class CollectionFilterWidget;
|
||||
class DeviceManager;
|
||||
class StreamingServices;
|
||||
class AlbumCoverLoader;
|
||||
class CurrentAlbumCoverLoader;
|
||||
class CoverProviders;
|
||||
class LyricsProviders;
|
||||
class EditTagDialog;
|
||||
class OrganizeDialog;
|
||||
|
||||
@@ -71,18 +58,8 @@ class CollectionView : public AutoExpandingTreeView {
|
||||
// Please note that the selection is recursive meaning that if for example an album is selected this will return all of it's songs.
|
||||
SongList GetSelectedSongs() const;
|
||||
|
||||
void Init(const SharedPtr<TaskManager> task_manager,
|
||||
const SharedPtr<TagReaderClient> tagreader_client,
|
||||
const SharedPtr<NetworkAccessManager> network,
|
||||
const SharedPtr<AlbumCoverLoader> albumcover_loader,
|
||||
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader,
|
||||
const SharedPtr<CoverProviders> cover_providers,
|
||||
const SharedPtr<LyricsProviders> lyrics_providers,
|
||||
const SharedPtr<CollectionLibrary> collection,
|
||||
const SharedPtr<DeviceManager> device_manager,
|
||||
const SharedPtr<StreamingServices> streaming_services);
|
||||
|
||||
void SetFilterWidget(CollectionFilterWidget *filter_widget);
|
||||
void SetApplication(Application *app);
|
||||
void SetFilter(CollectionFilterWidget *filter);
|
||||
|
||||
// QTreeView
|
||||
void keyboardSearch(const QString &search) override;
|
||||
@@ -106,7 +83,7 @@ class CollectionView : public AutoExpandingTreeView {
|
||||
void EditTagError(const QString &message);
|
||||
|
||||
Q_SIGNALS:
|
||||
void ShowSettingsDialog();
|
||||
void ShowConfigDialog();
|
||||
|
||||
void TotalSongCountUpdated_();
|
||||
void TotalArtistCountUpdated_();
|
||||
@@ -144,21 +121,8 @@ class CollectionView : public AutoExpandingTreeView {
|
||||
void SaveContainerPath(const QModelIndex &child);
|
||||
|
||||
private:
|
||||
SharedPtr<TaskManager> task_manager_;
|
||||
SharedPtr<TagReaderClient> tagreader_client_;
|
||||
SharedPtr<NetworkAccessManager> network_;
|
||||
SharedPtr<DeviceManager> device_manager_;
|
||||
SharedPtr<AlbumCoverLoader> albumcover_loader_;
|
||||
SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader_;
|
||||
SharedPtr<CollectionLibrary> collection_;
|
||||
SharedPtr<CoverProviders> cover_providers_;
|
||||
SharedPtr<LyricsProviders> lyrics_providers_;
|
||||
SharedPtr<StreamingServices> streaming_services_;
|
||||
|
||||
SharedPtr<CollectionBackend> backend_;
|
||||
CollectionModel *model_;
|
||||
CollectionFilter *filter_;
|
||||
CollectionFilterWidget *filter_widget_;
|
||||
Application *app_;
|
||||
CollectionFilterWidget *filter_;
|
||||
|
||||
int total_song_count_;
|
||||
int total_artist_count_;
|
||||
@@ -176,7 +140,7 @@ class CollectionView : public AutoExpandingTreeView {
|
||||
QAction *action_organize_;
|
||||
QAction *action_search_for_this_;
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
#ifndef Q_OS_WIN
|
||||
QAction *action_copy_to_device_;
|
||||
#endif
|
||||
QAction *action_edit_track_;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user