Compare commits

..

1 Commits

Author SHA1 Message Date
Jonas Kvinge
306709f498 discord-rpc: Port to Qt Json 2025-04-13 12:24:59 +02:00
499 changed files with 7981 additions and 21303 deletions

View File

@@ -130,10 +130,7 @@ InsertBraces: false
InsertTrailingCommas: None
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLines:
AtEndOfFile: true
AtStartOfBlock: true
AtStartOfFile: true
KeepEmptyLinesAtTheStartOfBlocks: true
LambdaBodyIndentation: Signature
MacroBlockBegin: ''
MacroBlockEnd: ''

View File

@@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
opensuse_version: [ 'tumbleweed', 'leap:15.6', 'leap:16.0' ]
opensuse_version: [ 'tumbleweed', 'leap:15.6' ]
container:
image: opensuse/${{matrix.opensuse_version}}
steps:
@@ -27,11 +27,11 @@ jobs:
- name: Upgrade packages (Leap)
if: matrix.opensuse_version != 'tumbleweed'
run: zypper -n --gpg-auto-import-keys up
- name: Install gcc
if: matrix.opensuse_version != 'leap:15.6'
- name: Install gcc (Tumbleweed)
if: matrix.opensuse_version == 'tumbleweed'
run: zypper -n --gpg-auto-import-keys in gcc gcc-c++
- name: Install gcc (leap:15.6)
if: matrix.opensuse_version == 'leap:15.6'
- name: Install gcc (Leap)
if: matrix.opensuse_version != 'tumbleweed'
run: zypper -n --gpg-auto-import-keys in gcc14 gcc14-c++
- name: Install packages
run: >
@@ -62,7 +62,6 @@ jobs:
libchromaprint-devel
fftw3-devel
libebur128-devel
projectM-devel
desktop-file-utils
update-desktop-files
appstream-glib
@@ -79,13 +78,12 @@ jobs:
qt6-base-common-devel
qt6-sql-sqlite
qt6-linguist-devel
qt6-openglwidgets-devel
gtest
gmock
sparsehash-devel
rapidjson-devel
- name: Install kdsingleapplication-qt6-devel
if: matrix.opensuse_version != 'leap:15.6'
if: matrix.opensuse_version == 'tumbleweed'
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
- name: Build and install KDSingleApplication
if: matrix.opensuse_version == 'leap:15.6'
@@ -99,7 +97,7 @@ jobs:
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@@ -117,14 +115,14 @@ jobs:
- name: Copy source tarball
working-directory: build
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
- name: Build RPM
if: matrix.opensuse_version != 'leap:15.6'
- name: Build RPM (Tumbleweed)
if: matrix.opensuse_version == 'tumbleweed'
env:
RPM_BUILD_NCPUS: 4
working-directory: build
run: rpmbuild -ba strawberry.spec
- name: Build RPM (leap:15.6)
if: matrix.opensuse_version == 'leap:15.6'
- name: Build RPM (Leap)
if: matrix.opensuse_version != 'tumbleweed'
env:
RPM_BUILD_NCPUS: 4
CC: gcc-14
@@ -136,14 +134,14 @@ jobs:
run: echo "subdir=$(echo ${{matrix.opensuse_version}} | sed 's/leap:/lp/g' | sed 's/\.//g')" > $GITHUB_OUTPUT
- name: Upload source
if: matrix.opensuse_version == 'tumbleweed'
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: source
path: |
/usr/src/packages/SOURCES/*.xz
- name: Upload rpm
if: matrix.opensuse_version != 'tumbleweed'
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: opensuse-${{steps.set-subdir.outputs.subdir}}
path: |
@@ -158,7 +156,7 @@ jobs:
strategy:
fail-fast: false
matrix:
fedora_version: [ '41', '42', '43' ]
fedora_version: [ '39', '40', '41', '42' ]
container:
image: fedora:${{matrix.fedora_version}}
steps:
@@ -202,7 +200,6 @@ jobs:
libchromaprint-devel
libebur128-devel
fftw-devel
libprojectM-devel
desktop-file-utils
libappstream-glib
hicolor-icon-theme
@@ -212,7 +209,7 @@ jobs:
sparsehash-devel
rapidjson-devel
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@@ -237,7 +234,7 @@ jobs:
working-directory: build
run: rpmbuild -ba strawberry.spec
- name: Upload artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: fedora-${{matrix.fedora_version}}
path: |
@@ -293,7 +290,6 @@ jobs:
lib64Qt6DBus-devel
lib64Qt6Gui-devel
lib64Qt6Widgets-devel
lib64Qt6OpenGLWidgets-devel
lib64Qt6Test-devel
lib64kdsingleapplication-devel
lib64xkbcommon-devel
@@ -311,7 +307,7 @@ jobs:
- name: Remove files
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@@ -337,7 +333,7 @@ jobs:
run: rpmbuild -ba strawberry.spec
- name: Upload artifacts
if: matrix.openmandriva_version != 'cooker'
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: openmandriva-${{matrix.openmandriva_version}}
path: |
@@ -388,7 +384,6 @@ jobs:
lib64fftw-devel
lib64dbus-devel
lib64appstream-devel
lib64projectm-devel
lib64qt6core-devel
lib64qt6gui-devel
lib64qt6widgets-devel
@@ -398,7 +393,6 @@ jobs:
lib64qt6dbus-devel
lib64qt6help-devel
lib64qt6test-devel
lib64qt6openglwidgets-devel
lib64sparsehash-devel
lib64kdsingleapplication-devel
desktop-file-utils
@@ -415,7 +409,7 @@ jobs:
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@@ -440,7 +434,7 @@ jobs:
working-directory: build
run: rpmbuild -ba strawberry.spec
- name: Upload artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: mageia-${{matrix.mageia_version}}
path: |
@@ -455,7 +449,7 @@ jobs:
strategy:
fail-fast: false
matrix:
debian_version: [ 'bookworm', 'trixie', 'forky' ]
debian_version: [ 'bookworm', 'trixie' ]
container:
image: debian:${{matrix.debian_version}}
steps:
@@ -505,12 +499,7 @@ jobs:
qt6-tools-dev-tools
qt6-l10n-tools
rapidjson-dev
libprojectm-dev
- name: Install KDSingleApplication
if: matrix.debian_version != 'bookworm'
run: apt install -y libkdsingleapplication-qt6-dev
- name: Build and install KDSingleApplication
if: matrix.debian_version == 'bookworm'
run: |
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
cd KDSingleApplication
@@ -518,7 +507,7 @@ jobs:
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@@ -535,7 +524,7 @@ jobs:
- name: Copy deb
run: cp ../*.deb .
- name: Upload artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: debian-${{matrix.debian_version}}
path: |
@@ -549,7 +538,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
container:
image: ubuntu:${{matrix.ubuntu_version}}
steps:
@@ -602,12 +591,7 @@ jobs:
qt6-tools-dev-tools
qt6-l10n-tools
rapidjson-dev
libprojectm-dev
- name: Install KDSingleApplication
if: matrix.ubuntu_version != 'noble' && matrix.ubuntu_version != 'plucky'
run: apt install -y libkdsingleapplication-qt6-dev
- name: Build and install KDSingleApplication
if: matrix.ubuntu_version == 'noble' || matrix.ubuntu_version == 'plucky'
run: |
git clone --depth 1 --recurse-submodules https://github.com/KDAB/KDSingleApplication
cd KDSingleApplication
@@ -615,7 +599,7 @@ jobs:
cmake --build build --config Release --parallel 4
cmake --install build
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@@ -632,7 +616,7 @@ jobs:
- name: Copy deb
run: cp ../*.deb ../*.ddeb .
- name: Upload artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: ubuntu-${{matrix.ubuntu_version}}
path: |
@@ -647,7 +631,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ubuntu_version: [ 'noble', 'plucky', 'questing' ]
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
container:
image: ubuntu:${{matrix.ubuntu_version}}
steps:
@@ -701,14 +685,13 @@ jobs:
gstreamer1.0-pulseaudio
libkdsingleapplication-qt6-dev
rapidjson-dev
libprojectm-dev
- name: Install keyboxd
if: matrix.ubuntu_version == 'noble'
env:
DEBIAN_FRONTEND: noninteractive
run: apt install -y keyboxd
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@@ -744,22 +727,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Free disk space
run: |
df -h
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
sudo apt-get clean
df -h
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.3.2
uses: vmactions/freebsd-vm@v1.2.0
with:
usesh: true
mem: 8192
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 sparsehash rapidjson
run: |
set -e
@@ -775,13 +752,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Build OpenBSD
id: build-openbsd
uses: vmactions/openbsd-vm@v1.2.9
uses: vmactions/openbsd-vm@v1.1.7
with:
usesh: true
mem: 4096
@@ -802,7 +779,7 @@ jobs:
strategy:
fail-fast: false
matrix:
runner: [ 'macos-15-intel', 'macos-15' ]
runner: [ 'macos-13', 'macos-15' ]
buildtype: [ 'release' ]
runs-on: ${{ matrix.runner }}
@@ -811,7 +788,7 @@ jobs:
- name: Set MACOSX_DEPLOYMENT_TARGET
run: |
for i in 12 13 14 15; do
for i in 13 14 15; do
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
echo "Using macOS SDK ${i}"
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
@@ -841,20 +818,20 @@ jobs:
rm -f uninstall.sh
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Import certificate file
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false
uses: apple-actions/import-codesign-certs@v6
uses: apple-actions/import-codesign-certs@v5
with:
p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
- name: Download macOS dependencies
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies$(test "${{env.arch}}" = "x86_64" && echo "-intel" || echo "")/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
- name: Extract macOS dependencies
run: sudo tar -C / -xf strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
@@ -905,9 +882,9 @@ jobs:
run: make deploy
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-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,libbrotlicommon.1.dylib,libbrotlienc.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'
@@ -969,7 +946,7 @@ jobs:
- name: Set MACOSX_DEPLOYMENT_TARGET
run: |
for i in 12 13 14 15; do
for i in 13 14 15; do
if [ -d "/Library/Developer/CommandLineTools/SDKs/MacOSX${i}.sdk" ]; then
echo "Using macOS SDK ${i}"
echo "MACOSX_DEPLOYMENT_TARGET=${i}.0" >> $GITHUB_ENV
@@ -992,7 +969,7 @@ jobs:
run: echo "cmake_buildtype=$(echo ${{env.buildtype}} | awk '{print toupper(substr($0,0,1))tolower(substr($0,2))}')" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@@ -1095,7 +1072,7 @@ jobs:
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@@ -1269,42 +1246,12 @@ jobs:
build-windows-msvc:
name: Build Windows MSVC
if: github.repository != 'strawberrymusicplayer/strawberry-private' && github.ref != 'refs/heads/l10n_master'
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
include:
- name: "x86_64 debug"
runner: windows-2022
arch: x86_64
buildtype: debug
- name: "x86_64 release"
runner: windows-2022
arch: x86_64
buildtype: release
- name: "x86 debug"
runner: windows-2022
arch: x86
buildtype: debug
- name: "x86 release"
runner: windows-2022
arch: x86
buildtype: release
- name: "arm64 debug"
runner: windows-11-arm
arch: arm64
buildtype: debug
- name: "arm64 release"
runner: windows-11-arm
arch: arm64
buildtype: release
runs-on: ${{matrix.runner}}
arch: [ 'x86', 'x86_64' ]
buildtype: [ 'release' ]
steps:
- name: Set prefix path
@@ -1318,20 +1265,6 @@ jobs:
shell: bash
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
- name: Show SDK versions
shell: bash
run: ls -la "c:/Program Files (x86)/Windows Kits/10/include"
- name: Set SDK version
if: matrix.arch != 'arm64'
shell: bash
run: echo "sdk_version=10.0.22621.0" >> $GITHUB_ENV
- name: Set SDK version
if: matrix.arch == 'arm64'
shell: bash
run: echo "sdk_version=10.0.26100.0" >> $GITHUB_ENV
- name: Install rsync
shell: cmd
run: choco install --no-progress rsync
@@ -1360,9 +1293,7 @@ jobs:
- name: Copy bin files
shell: bash
run: |
cp /c/mingw64/bin/{strip.exe,strings.exe,objdump.exe} ${{env.prefix_path_unix}}/bin
cp /c/strawberry/c/bin/patch.exe ${{env.prefix_path_unix}}/bin
run: cp /c/strawberry/c/bin/{patch.exe,strip.exe,strings.exe,objdump.exe} ${{env.prefix_path_unix}}/bin
- name: Delete conflicting files
shell: bash
@@ -1416,11 +1347,11 @@ jobs:
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{matrix.arch}}
sdk: ${{env.sdk_version}}
sdk: 10.0.20348.0
vsversion: 2022
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
@@ -1433,18 +1364,15 @@ jobs:
shell: cmd
run: cmake -E make_directory build
- name: Set ENABLE_WIN32_CONSOLE
- name: Set ENABLE_WIN32_CONSOLE (debug)
if: matrix.buildtype == 'debug'
shell: bash
run: echo "enable_win32_console=$(test "${{matrix.buildtype}}" = "debug" && echo "ON" || echo "OFF")" >> $GITHUB_ENV
run: echo "win32_console=ON" >> $GITHUB_ENV
- name: Set ENABLE_SPOTIFY
- name: Set ENABLE_WIN32_CONSOLE (release)
if: matrix.buildtype == 'release'
shell: bash
run: echo "enable_spotify=$(test -f "${{env.prefix_path_unix}}/lib/gstreamer-1.0/gstspotify.dll" && echo "ON" || echo "OFF")" >> $GITHUB_ENV
- name: Remove -lm from .pc files
if: matrix.arch == 'arm64'
shell: bash
run: sed -i 's/\-lm$//g' ${{env.prefix_path_unix}}/lib/pkgconfig/*.pc
run: echo "win32_console=OFF" >> $GITHUB_ENV
- name: Run CMake
shell: cmd
@@ -1456,14 +1384,14 @@ jobs:
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
-DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake"
-DARCH="${{matrix.arch}}"
-DENABLE_WIN32_CONSOLE=${{env.enable_win32_console}}
-DENABLE_WIN32_CONSOLE=${{env.win32_console}}
-DPKG_CONFIG_EXECUTABLE="${{env.prefix_path_forwardslash}}/bin/pkg-config.exe"
-DICU_ROOT="${{env.prefix_path_forwardslash}}"
-DENABLE_GIO=OFF
-DENABLE_AUDIOCD=OFF
-DENABLE_MTP=OFF
-DENABLE_GPOD=OFF
-DENABLE_SPOTIFY=${{env.enable_spotify}}
-DENABLE_SPOTIFY=ON
- name: Run Make
shell: cmd
@@ -1653,11 +1581,11 @@ jobs:
DEBIAN_FRONTEND: noninteractive
run: sudo apt install -y git rsync
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v4
with:
path: artifacts
- name: SSH Setup
@@ -1701,7 +1629,7 @@ jobs:
DEBIAN_FRONTEND: noninteractive
run: sudo apt install -y git jq gh
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Show release assets
@@ -1709,7 +1637,7 @@ jobs:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: gh release view "${{github.event.release.tag_name}}" --json assets | jq -r '.assets[].name'
- name: Download artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Add artifacts to release

2
.gitignore vendored
View File

@@ -1,7 +1,6 @@
/build
/bin
/CMakeLists.txt.user
/.qtcreator
/.kdev4
/strawberry.kdev4
/.vscode
@@ -13,4 +12,3 @@
/CMakeSettings.json
/dist/scripts/maketarball.sh
/debian/changelog
_codeql_detected_source_root

View File

@@ -37,5 +37,6 @@ if(WIN32)
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
endif()
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
target_link_libraries(discord-rpc PRIVATE Qt${QT_VERSION_MAJOR}::Core)
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -24,11 +24,13 @@
#ifndef DISCORD_REGISTER_H
#define DISCORD_REGISTER_H
#include <QString>
#ifdef __cplusplus
extern "C" {
#endif
void Discord_Register(const char *applicationId, const char *command);
void Discord_Register(const QString &applicationId, const char *command);
#ifdef __cplusplus
}

View File

@@ -21,9 +21,6 @@
*
*/
#include "discord_rpc.h"
#include "discord_register.h"
#include <cstdio>
#include <errno.h>
#include <cstdlib>
@@ -32,6 +29,11 @@
#include <sys/types.h>
#include <unistd.h>
#include <QString>
#include "discord_rpc.h"
#include "discord_register.h"
namespace {
static bool Mkdir(const char *path) {
@@ -48,7 +50,7 @@ static bool Mkdir(const char *path) {
} // namespace
// We want to register games so we can run them from Discord client as discord-<appid>://
extern "C" void Discord_Register(const char *applicationId, const char *command) {
extern "C" void Discord_Register(const QString &applicationId, const char *command) {
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
@@ -75,13 +77,13 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
"Categories=Discord;Games;\n"
"MimeType=x-scheme-handler/discord-%s;\n";
char desktopFile[2048]{};
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId.toUtf8().constData(), command, applicationId.toUtf8().constData());
if (fileLen <= 0) {
return;
}
char desktopFilename[256]{};
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId.toUtf8().constData());
char desktopFilePath[1024]{};
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
@@ -111,8 +113,8 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
snprintf(xdgMimeCommand,
sizeof(xdgMimeCommand),
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
applicationId,
applicationId);
applicationId.toUtf8().constData(),
applicationId.toUtf8().constData());
if (system(xdgMimeCommand) < 0) {
fprintf(stderr, "Failed to register mime handler\n");
}

View File

@@ -84,15 +84,17 @@ static void RegisterURL(const char *applicationId) {
}
void Discord_Register(const char *applicationId, const char *command) {
void Discord_Register(const QString &applicationId, const char *command) {
const QByteArray applicationIdData = applicationId.toUtf8();
if (command) {
RegisterCommand(applicationId, command);
RegisterCommand(applicationIdData.constData(), command);
}
else {
// raii lite
@autoreleasepool {
RegisterURL(applicationId);
RegisterURL(applicationIdData.constData());
}
}

View File

@@ -147,10 +147,10 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
}
extern "C" void Discord_Register(const char *applicationId, const char *command) {
extern "C" void Discord_Register(const QString &applicationId, const char *command) {
wchar_t appId[32]{};
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
MultiByteToWideChar(CP_UTF8, 0, applicationId.toUtf8().constData(), -1, appId, 32);
wchar_t openCommand[1024]{};
const wchar_t *wcommand = nullptr;
@@ -163,3 +163,4 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
Discord_RegisterW(appId, wcommand);
}

View File

@@ -27,6 +27,10 @@
#include <condition_variable>
#include <thread>
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_rpc.h"
#include "discord_backoff.h"
#include "discord_register.h"
@@ -34,11 +38,9 @@
#include "discord_rpc_connection.h"
#include "discord_serialization.h"
using namespace discord_rpc;
using namespace Qt::Literals::StringLiterals;
static void Discord_UpdateConnection();
namespace {
namespace discord_rpc {
constexpr size_t MaxMessageSize { 16 * 1024 };
constexpr size_t MessageQueueSize { 8 };
@@ -56,17 +58,19 @@ struct QueuedMessage {
}
};
struct User {
class User {
public:
explicit User() {}
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
// terminator = 21
char userId[32];
QString userId;
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
// terminator = 129
char username[344];
QString username;
// 4 decimal digits + 1 null terminator = 5
char discriminator[8];
QString discriminator;
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
char avatar[128];
QString avatar;
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
};
@@ -79,12 +83,12 @@ static std::atomic_bool GotErrorMessage{ false };
static std::atomic_bool WasJoinGame { false };
static std::atomic_bool WasSpectateGame { false };
static std::atomic_bool UpdatePresence { false };
static char JoinGameSecret[256];
static char SpectateGameSecret[256];
static QString JoinGameSecret;
static QString SpectateGameSecret;
static int LastErrorCode { 0 };
static char LastErrorMessage[256];
static QString LastErrorMessage;
static int LastDisconnectErrorCode { 0 };
static char LastDisconnectErrorMessage[256];
static QString LastDisconnectErrorMessage;
static std::mutex PresenceMutex;
static std::mutex HandlerMutex;
static QueuedMessage QueuedPresence {};
@@ -98,6 +102,7 @@ static auto NextConnect = std::chrono::system_clock::now();
static int Pid { 0 };
static int Nonce { 1 };
static void Discord_UpdateConnection(void);
class IoThreadHolder {
private:
std::atomic_bool keepRunning { true };
@@ -131,7 +136,6 @@ class IoThreadHolder {
~IoThreadHolder() { Stop(); }
};
static IoThreadHolder *IoThread { nullptr };
static void UpdateReconnectTime() {
@@ -140,6 +144,110 @@ static void UpdateReconnectTime() {
}
static void Discord_UpdateConnection() {
if (!Connection) {
return;
}
if (!Connection->IsOpen()) {
if (std::chrono::system_clock::now() >= NextConnect) {
UpdateReconnectTime();
Connection->Open();
}
}
else {
// reads
for (;;) {
QJsonDocument json_document;
if (!Connection->Read(json_document)) {
break;
}
const QJsonObject json_object = json_document.object();
const QString event_name = json_object["evt"_L1].toString();
const QString nonce = json_object["nonce"_L1].toString();
if (json_object.contains("nonce"_L1)) {
// in responses only -- should use to match up response when needed.
if (event_name == "ERROR"_L1) {
const QJsonObject data = json_object["data"_L1].toObject();
LastErrorCode = data["code"_L1].toInt();
LastErrorMessage = data["message"_L1].toString();
GotErrorMessage.store(true);
}
}
else {
// should have evt == name of event, optional data
if (event_name.isEmpty()) {
continue;
}
const QJsonObject data = json_object["data"_L1].toObject();
if (event_name == "ACTIVITY_JOIN"_L1) {
if (data.contains("secret"_L1)) {
JoinGameSecret = data["secret"_L1].toString();
WasJoinGame.store(true);
}
}
else if (event_name == "ACTIVITY_SPECTATE"_L1) {
if (data.contains("secret"_L1)) {
SpectateGameSecret = data["secret"_L1].toString();
WasSpectateGame.store(true);
}
}
else if (event_name == "ACTIVITY_JOIN_REQUEST"_L1) {
const QJsonObject user = data["user"_L1].toObject();
const QString userId = user["id"_L1].toString();
const QString username = user["username"_L1].toString();
const QString avatar = user["avatar"_L1].toString();
const auto joinReq = JoinAskQueue.GetNextAddMessage();
if (!userId.isEmpty() && !username.isEmpty() && joinReq) {
joinReq->userId = userId;
joinReq->username = username;
const QString discriminator = user["discriminator"_L1].toString();
if (!discriminator.isEmpty()) {
joinReq->discriminator = discriminator;
}
if (!avatar.isEmpty()) {
joinReq->avatar = avatar;
}
else {
joinReq->avatar.clear();
}
JoinAskQueue.CommitAdd();
}
}
}
}
// writes
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
QueuedMessage local;
{
std::lock_guard<std::mutex> guard(PresenceMutex);
local.Copy(QueuedPresence);
}
if (!Connection->Write(local.buffer, local.length)) {
// if we fail to send, requeue
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.Copy(local);
UpdatePresence.exchange(true);
}
}
while (SendQueue.HavePendingSends()) {
auto qmessage = SendQueue.GetNextSendMessage();
Connection->Write(qmessage->buffer, qmessage->length);
SendQueue.CommitSend();
}
}
}
static void SignalIOActivity() {
if (IoThread != nullptr) {
@@ -176,115 +284,7 @@ static bool DeregisterForEvent(const char *evtName) {
}
} // namespace
static void Discord_UpdateConnection() {
if (!Connection) {
return;
}
if (!Connection->IsOpen()) {
if (std::chrono::system_clock::now() >= NextConnect) {
UpdateReconnectTime();
Connection->Open();
}
}
else {
// reads
for (;;) {
JsonDocument message;
if (!Connection->Read(message)) {
break;
}
const char *evtName = GetStrMember(&message, "evt");
const char *nonce = GetStrMember(&message, "nonce");
if (nonce) {
// in responses only -- should use to match up response when needed.
if (evtName && strcmp(evtName, "ERROR") == 0) {
auto data = GetObjMember(&message, "data");
LastErrorCode = GetIntMember(data, "code");
StringCopy(LastErrorMessage, GetStrMember(data, "message", ""));
GotErrorMessage.store(true);
}
}
else {
// should have evt == name of event, optional data
if (evtName == nullptr) {
continue;
}
auto data = GetObjMember(&message, "data");
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) {
auto secret = GetStrMember(data, "secret");
if (secret) {
StringCopy(JoinGameSecret, secret);
WasJoinGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) {
auto secret = GetStrMember(data, "secret");
if (secret) {
StringCopy(SpectateGameSecret, secret);
WasSpectateGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) {
auto user = GetObjMember(data, "user");
auto userId = GetStrMember(user, "id");
auto username = GetStrMember(user, "username");
auto avatar = GetStrMember(user, "avatar");
auto joinReq = JoinAskQueue.GetNextAddMessage();
if (userId && username && joinReq) {
StringCopy(joinReq->userId, userId);
StringCopy(joinReq->username, username);
auto discriminator = GetStrMember(user, "discriminator");
if (discriminator) {
StringCopy(joinReq->discriminator, discriminator);
}
if (avatar) {
StringCopy(joinReq->avatar, avatar);
}
else {
joinReq->avatar[0] = 0;
}
JoinAskQueue.CommitAdd();
}
}
}
}
// writes
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
QueuedMessage local;
{
std::lock_guard<std::mutex> guard(PresenceMutex);
local.Copy(QueuedPresence);
}
if (!Connection->Write(local.buffer, local.length)) {
// if we fail to send, requeue
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.Copy(local);
UpdatePresence.exchange(true);
}
}
while (SendQueue.HavePendingSends()) {
auto qmessage = SendQueue.GetNextSendMessage();
Connection->Write(qmessage->buffer, qmessage->length);
SendQueue.CommitSend();
}
}
}
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
extern "C" void Discord_Initialize(const QString &applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
IoThread = new (std::nothrow) IoThreadHolder();
if (IoThread == nullptr) {
@@ -315,37 +315,38 @@ extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandle
}
Connection = RpcConnection::Create(applicationId);
Connection->onConnect = [](JsonDocument &readyMessage) {
Connection->onConnect = [](QJsonDocument &readyMessage) {
Discord_UpdateHandlers(&QueuedHandlers);
if (QueuedPresence.length > 0) {
UpdatePresence.exchange(true);
SignalIOActivity();
}
auto data = GetObjMember(&readyMessage, "data");
auto user = GetObjMember(data, "user");
auto userId = GetStrMember(user, "id");
auto username = GetStrMember(user, "username");
auto avatar = GetStrMember(user, "avatar");
if (userId && username) {
StringCopy(connectedUser.userId, userId);
StringCopy(connectedUser.username, username);
auto discriminator = GetStrMember(user, "discriminator");
if (discriminator) {
StringCopy(connectedUser.discriminator, discriminator);
const QJsonValue json_object = readyMessage.object();
auto data = json_object["data"_L1].toObject();
auto user = data["user"_L1].toObject();
auto userId = user["id"_L1].toString();
auto username = user["username"_L1].toString();
auto avatar = user["avatar"_L1].toString();
if (!userId.isEmpty() && !username.isEmpty()) {
connectedUser.userId = userId;
connectedUser.username = username;
const QString discriminator = user["discriminator"_L1].toString();
if (!discriminator.isEmpty()) {
connectedUser.discriminator = discriminator;
}
if (avatar) {
StringCopy(connectedUser.avatar, avatar);
if (!avatar.isEmpty()) {
connectedUser.avatar = avatar;
}
else {
connectedUser.avatar[0] = 0;
connectedUser = User();
}
}
WasJustConnected.exchange(true);
ReconnectTimeMs.reset();
};
Connection->onDisconnect = [](int err, const char *message) {
Connection->onDisconnect = [](int err, QString &message) {
LastDisconnectErrorCode = err;
StringCopy(LastDisconnectErrorMessage, message);
LastDisconnectErrorMessage = message;
WasJustDisconnected.exchange(true);
UpdateReconnectTime();
};
@@ -354,7 +355,7 @@ extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandle
}
extern "C" void Discord_Shutdown() {
extern "C" void Discord_Shutdown(void) {
if (!Connection) {
return;
@@ -374,7 +375,7 @@ extern "C" void Discord_Shutdown() {
}
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
extern "C" void Discord_UpdatePresence(const DiscordRichPresence &presence) {
{
std::lock_guard<std::mutex> guard(PresenceMutex);
@@ -386,8 +387,8 @@ extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
}
extern "C" void Discord_ClearPresence(void) {
Discord_UpdatePresence(nullptr);
extern "C" void Discord_ClearPresence() {
Discord_UpdatePresence();
}
extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) {
@@ -508,3 +509,5 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
}
}
} // namespace discord_rpc

View File

@@ -25,46 +25,49 @@
#define DISCORD_RPC_H
#include <cstdint>
#include <QString>
namespace discord_rpc {
#ifdef __cplusplus
extern "C" {
#endif
typedef struct DiscordRichPresence {
class DiscordRichPresence {
public:
int type;
int status_display_type;
const char *name; /* max 128 bytes */
const char *state; /* max 128 bytes */
const char *details; /* max 128 bytes */
int64_t startTimestamp;
int64_t endTimestamp;
const char *largeImageKey; /* max 32 bytes */
const char *largeImageText; /* max 128 bytes */
const char *smallImageKey; /* max 32 bytes */
const char *smallImageText; /* max 128 bytes */
const char *partyId; /* max 128 bytes */
QString name; /* max 128 bytes */
QString state; /* max 128 bytes */
QString details; /* max 128 bytes */
qint64 startTimestamp;
qint64 endTimestamp;
QString largeImageKey; /* max 32 bytes */
QString largeImageText; /* max 128 bytes */
QString smallImageKey; /* max 32 bytes */
QString smallImageText; /* max 128 bytes */
QString partyId; /* max 128 bytes */
int partySize;
int partyMax;
int partyPrivacy;
const char *matchSecret; /* max 128 bytes */
const char *joinSecret; /* max 128 bytes */
const char *spectateSecret; /* max 128 bytes */
int8_t instance;
} DiscordRichPresence;
QString matchSecret; /* max 128 bytes */
QString joinSecret; /* max 128 bytes */
QString spectateSecret; /* max 128 bytes */
qint8 instance;
};
typedef struct DiscordUser {
const char *userId;
const char *username;
const char *discriminator;
const char *avatar;
const QString userId;
const QString username;
const QString discriminator;
const QString avatar;
} DiscordUser;
typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser *request);
void (*disconnected)(int errorCode, const char *message);
void (*errored)(int errorCode, const char *message);
void (*joinGame)(const char *joinSecret);
void (*spectateGame)(const char *spectateSecret);
void (*disconnected)(int errorCode, const QString &message);
void (*errored)(int errorCode, const QString &message);
void (*joinGame)(const QString &joinSecret);
void (*spectateGame)(const QString &spectateSecret);
void (*joinRequest)(const DiscordUser *request);
} DiscordEventHandlers;
@@ -74,14 +77,14 @@ typedef struct DiscordEventHandlers {
#define DISCORD_PARTY_PRIVATE 0
#define DISCORD_PARTY_PUBLIC 1
void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister);
void Discord_Shutdown(void);
void Discord_Initialize(const QString &applicationId, DiscordEventHandlers *handlers, const int autoRegister);
void Discord_Shutdown();
// checks for incoming messages, dispatches callbacks
void Discord_RunCallbacks(void);
void Discord_RunCallbacks();
void Discord_UpdatePresence(const DiscordRichPresence *presence);
void Discord_ClearPresence(void);
void Discord_UpdatePresence(const DiscordRichPresence &presence = DiscordRichPresence());
void Discord_ClearPresence();
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
@@ -91,4 +94,6 @@ void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
} /* extern "C" */
#endif
} // namespace discord_rpc
#endif // DISCORD_RPC_H

View File

@@ -21,18 +21,24 @@
*
*/
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_rpc_connection.h"
#include "discord_serialization.h"
using namespace Qt::Literals::StringLiterals;
namespace discord_rpc {
static const int RpcVersion = 1;
static RpcConnection Instance;
RpcConnection *RpcConnection::Create(const char *applicationId) {
RpcConnection *RpcConnection::Create(const QString &applicationId) {
Instance.connection = BaseConnection::Create();
StringCopy(Instance.appId, applicationId);
Instance.appId = applicationId;
return &Instance;
}
@@ -56,14 +62,15 @@ void RpcConnection::Open() {
}
if (state == State::SentHandshake) {
JsonDocument message;
if (Read(message)) {
auto cmd = GetStrMember(&message, "cmd");
auto evt = GetStrMember(&message, "evt");
if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) {
QJsonDocument json_document;
if (Read(json_document)) {
const QJsonObject json_object = json_document.object();
const QString cmd = json_object["cmd"_L1].toString();
const QString evt = json_object["evt"_L1].toString();
if (cmd == "DISPATCH"_L1 && evt == "READY"_L1) {
state = State::Connected;
if (onConnect) {
onConnect(message);
onConnect(json_document);
}
}
}
@@ -106,7 +113,7 @@ bool RpcConnection::Write(const void *data, size_t length) {
}
bool RpcConnection::Read(JsonDocument &message) {
bool RpcConnection::Read(QJsonDocument &message) {
if (state != State::Connected && state != State::SentHandshake) {
return false;
@@ -117,7 +124,7 @@ bool RpcConnection::Read(JsonDocument &message) {
if (!didRead) {
if (!connection->isOpen) {
lastErrorCode = static_cast<int>(ErrorCode::PipeClosed);
StringCopy(lastErrorMessage, "Pipe closed");
lastErrorMessage = "Pipe closed"_L1;
Close();
}
return false;
@@ -127,7 +134,7 @@ bool RpcConnection::Read(JsonDocument &message) {
didRead = connection->Read(readFrame.message, readFrame.length);
if (!didRead) {
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
StringCopy(lastErrorMessage, "Partial data in frame");
lastErrorMessage = "Partial data in frame"_L1;
Close();
return false;
}
@@ -136,14 +143,14 @@ bool RpcConnection::Read(JsonDocument &message) {
switch (readFrame.opcode) {
case Opcode::Close: {
message.ParseInsitu(readFrame.message);
lastErrorCode = GetIntMember(&message, "code");
StringCopy(lastErrorMessage, GetStrMember(&message, "message", ""));
message = QJsonDocument::fromJson(readFrame.message);
lastErrorCode = message["code"_L1].toInt();
lastErrorMessage = message["message"_L1].toString();
Close();
return false;
}
case Opcode::Frame:
message.ParseInsitu(readFrame.message);
message = QJsonDocument::fromJson(readFrame.message);
return true;
case Opcode::Ping:
readFrame.opcode = Opcode::Pong;
@@ -157,7 +164,7 @@ bool RpcConnection::Read(JsonDocument &message) {
default:
// something bad happened
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
StringCopy(lastErrorMessage, "Bad ipc frame");
lastErrorMessage = "Bad ipc frame"_L1;
Close();
return false;
}

View File

@@ -24,6 +24,9 @@
#ifndef DISCORD_RPC_CONNECTION_H
#define DISCORD_RPC_CONNECTION_H
#include <QString>
#include <QJsonDocument>
#include "discord_connection.h"
#include "discord_serialization.h"
@@ -65,14 +68,14 @@ struct RpcConnection {
BaseConnection *connection { nullptr };
State state { State::Disconnected };
void (*onConnect)(JsonDocument &message) { nullptr };
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
char appId[64]{};
void (*onConnect)(QJsonDocument &message) { nullptr };
void (*onDisconnect)(int errorCode, QString &message) { nullptr };
QString appId;
int lastErrorCode { 0 };
char lastErrorMessage[256]{};
QString lastErrorMessage;
RpcConnection::MessageFrame sendFrame;
static RpcConnection *Create(const char *applicationId);
static RpcConnection *Create(const QString &applicationId);
static void Destroy(RpcConnection *&);
inline bool IsOpen() const { return state == State::Connected; }
@@ -80,7 +83,7 @@ struct RpcConnection {
void Open();
void Close();
bool Write(const void *data, size_t length);
bool Read(JsonDocument &message);
bool Read(QJsonDocument &message);
};
} // namespace discord_rpc

View File

@@ -21,10 +21,15 @@
*
*/
#include <QString>
#include <QJsonDocument>
#include <QJsonObject>
#include "discord_serialization.h"
#include "discord_connection.h"
#include "discord_rpc.h"
using namespace Qt::Literals::StringLiterals;
namespace discord_rpc {
template<typename T>
@@ -53,232 +58,129 @@ void NumberToString(char *dest, T number) {
}
// it's ever so slightly faster to not have to strlen the key
template<typename T>
void WriteKey(JsonWriter &w, T &k) {
w.Key(k, sizeof(T) - 1);
}
void WriteOptionalString(QJsonObject &json_object, const QString &key, const QString &value);
void WriteOptionalString(QJsonObject &json_object, const QString &key, const QString &value) {
struct WriteObject {
JsonWriter &writer;
WriteObject(JsonWriter &w)
: writer(w) {
writer.StartObject();
}
template<typename T>
WriteObject(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartObject();
}
~WriteObject() { writer.EndObject(); }
};
struct WriteArray {
JsonWriter &writer;
template<typename T>
WriteArray(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartArray();
}
~WriteArray() { writer.EndArray(); }
};
template<typename T>
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
if (value && value[0]) {
w.Key(k, sizeof(T) - 1);
w.String(value);
if (!value.isEmpty()) {
json_object[key] = value;
}
}
static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
static QString JsonWriteNonce(const int nonce) {
WriteKey(writer, "nonce");
char nonceBuffer[32];
NumberToString(nonceBuffer, nonce);
writer.String(nonceBuffer);
char nonce_buffer[32]{};
NumberToString(nonce_buffer, nonce);
return QString::fromLatin1(nonce_buffer);
}
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence &presence) {
JsonWriter writer(dest, maxLen);
QJsonObject json_object;
{
WriteObject top(writer);
json_object["nonce"_L1] = JsonWriteNonce(nonce);
json_object["cmd"_L1] = "SET_ACTIVITY"_L1;
JsonWriteNonce(writer, nonce);
QJsonObject args;
args["pid"_L1] = pid;
WriteKey(writer, "cmd");
writer.String("SET_ACTIVITY");
QJsonObject activity;
{
WriteObject args(writer, "args");
WriteKey(writer, "pid");
writer.Int(pid);
if (presence != nullptr) {
WriteObject activity(writer, "activity");
if (presence->type >= 0 && presence->type <= 5) {
WriteKey(writer, "type");
writer.Int(presence->type);
WriteKey(writer, "status_display_type");
writer.Int(presence->status_display_type);
if (presence.type >= 0 && presence.type <= 5) {
activity["type"_L1] = presence.type;
}
WriteOptionalString(writer, "name", presence->name);
WriteOptionalString(writer, "state", presence->state);
WriteOptionalString(writer, "details", presence->details);
activity["state"_L1] = presence.state;
activity["details"_L1] = presence.details;
if (presence->startTimestamp || presence->endTimestamp) {
WriteObject timestamps(writer, "timestamps");
if (presence->startTimestamp) {
WriteKey(writer, "start");
writer.Int64(presence->startTimestamp);
if (presence.startTimestamp != 0 || presence.endTimestamp != 0) {
QJsonObject timestamps;
if (presence.startTimestamp != 0) {
timestamps["start"_L1] = presence.startTimestamp;
}
if (presence.endTimestamp != 0) {
timestamps["end"_L1] = presence.endTimestamp;
}
activity["timestamps"_L1] = timestamps;
}
if (presence->endTimestamp) {
WriteKey(writer, "end");
writer.Int64(presence->endTimestamp);
}
if (!presence.largeImageKey.isEmpty() || !presence.largeImageText.isEmpty() || !presence.smallImageKey.isEmpty() || !presence.smallImageText.isEmpty()) {
QJsonObject assets;
WriteOptionalString(assets, "large_image"_L1, presence.largeImageKey);
WriteOptionalString(assets, "large_text"_L1, presence.largeImageText);
WriteOptionalString(assets, "small_image"_L1, presence.smallImageKey);
WriteOptionalString(assets, "small_text"_L1, presence.smallImageText);
activity["assets"_L1] = assets;
}
if ((presence->largeImageKey && presence->largeImageKey[0]) ||
(presence->largeImageText && presence->largeImageText[0]) ||
(presence->smallImageKey && presence->smallImageKey[0]) ||
(presence->smallImageText && presence->smallImageText[0])) {
WriteObject assets(writer, "assets");
WriteOptionalString(writer, "large_image", presence->largeImageKey);
WriteOptionalString(writer, "large_text", presence->largeImageText);
WriteOptionalString(writer, "small_image", presence->smallImageKey);
WriteOptionalString(writer, "small_text", presence->smallImageText);
}
activity["instance"_L1] = presence.instance != 0;
args["activity"_L1] = activity;
json_object["args"_L1] = args;
if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
presence->partyMax || presence->partyPrivacy) {
WriteObject party(writer, "party");
WriteOptionalString(writer, "id", presence->partyId);
if (presence->partySize && presence->partyMax) {
WriteArray size(writer, "size");
writer.Int(presence->partySize);
writer.Int(presence->partyMax);
}
QJsonDocument json_document(json_object);
QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
if (presence->partyPrivacy) {
WriteKey(writer, "privacy");
writer.Int(presence->partyPrivacy);
}
}
if ((presence->matchSecret && presence->matchSecret[0]) ||
(presence->joinSecret && presence->joinSecret[0]) ||
(presence->spectateSecret && presence->spectateSecret[0])) {
WriteObject secrets(writer, "secrets");
WriteOptionalString(writer, "match", presence->matchSecret);
WriteOptionalString(writer, "join", presence->joinSecret);
WriteOptionalString(writer, "spectate", presence->spectateSecret);
}
writer.Key("instance");
writer.Bool(presence->instance != 0);
}
}
}
return writer.Size();
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
WriteKey(writer, "v");
writer.Int(version);
WriteKey(writer, "client_id");
writer.String(applicationId);
}
return writer.Size();
return data.length();
}
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
size_t JsonWriteHandshakeObj(char *dest, const size_t maxLen, const int version, const QString &applicationId) {
JsonWriter writer(dest, maxLen);
QJsonObject json_object;
json_object["v"_L1] = version;
json_object["client_id"_L1] = applicationId;
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
{
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("SUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
return data.length();
}
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
size_t JsonWriteSubscribeCommand(char *dest, const size_t maxLen, const int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen);
QJsonObject json_object;
json_object["nonce"_L1] = JsonWriteNonce(nonce);
json_object["cmd"_L1] = "SUBSCRIBE"_L1;
json_object["evt"_L1] = QLatin1String(evtName);
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
{
WriteObject obj(writer);
return data.length();
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("UNSUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
size_t JsonWriteUnsubscribeCommand(char *dest, const size_t maxLen, const int nonce, const char *evtName) {
QJsonObject json_object;
json_object["nonce"_L1] = JsonWriteNonce(nonce);
json_object["cmd"_L1] = "UNSUBSCRIBE"_L1;
json_object["evt"_L1] = QLatin1String(evtName);
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
return data.length();
}
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
JsonWriter writer(dest, maxLen);
QJsonObject json_object;
json_object["nonce"_L1] = JsonWriteNonce(nonce);
json_object["cmd"_L1] = reply == DISCORD_REPLY_YES ? "SEND_ACTIVITY_JOIN_INVITE"_L1 : "CLOSE_ACTIVITY_JOIN_REQUEST"_L1;
QJsonObject args;
args["user_id"_L1] = QLatin1String(userId);
json_object["args"_L1] = args;
const QJsonDocument json_document(json_object);
const QByteArray data = json_document.toJson(QJsonDocument::Compact);
strncpy(dest, data.constData(), maxLen);
{
WriteObject obj(writer);
WriteKey(writer, "cmd");
if (reply == DISCORD_REPLY_YES) {
writer.String("SEND_ACTIVITY_JOIN_INVITE");
}
else {
writer.String("CLOSE_ACTIVITY_JOIN_REQUEST");
}
WriteKey(writer, "args");
{
WriteObject args(writer);
WriteKey(writer, "user_id");
writer.String(userId);
}
JsonWriteNonce(writer, nonce);
}
return writer.Size();
return data.length();
}

View File

@@ -24,190 +24,18 @@
#ifndef DISCORD_SERIALIZATION_H
#define DISCORD_SERIALIZATION_H
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
struct DiscordRichPresence;
#include <cstddef>
#include <QString>
namespace discord_rpc {
// if only there was a standard library function for this
template<size_t Len>
inline size_t StringCopy(char (&dest)[Len], const char *src) {
if (!src || !Len) {
return 0;
}
size_t copied;
char *out = dest;
for (copied = 1; *src && copied < Len; ++copied) {
*out++ = *src++;
}
*out = 0;
return copied - 1;
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
// Commands
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const QString &applicationId);
class DiscordRichPresence;
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence &presence);
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce);
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need
// to supply some of your own allocators for stuff rather than use the defaults
class LinearAllocator {
public:
char *buffer_;
char *end_;
LinearAllocator() {
assert(0); // needed for some default case in rapidjson, should not use
}
LinearAllocator(char *buffer, size_t size)
: buffer_(buffer), end_(buffer + size) {
}
static const bool kNeedFree = false;
void *Malloc(size_t size) {
char *res = buffer_;
buffer_ += size;
if (buffer_ > end_) {
buffer_ = res;
return nullptr;
}
return res;
}
void *Realloc(void *originalPtr, size_t originalSize, size_t newSize) {
if (newSize == 0) {
return nullptr;
}
// allocate how much you need in the first place
assert(!originalPtr && !originalSize);
// unused parameter warning
(void)(originalPtr);
(void)(originalSize);
return Malloc(newSize);
}
static void Free(void *ptr) {
/* shrug */
(void)ptr;
}
};
template<size_t Size>
class FixedLinearAllocator : public LinearAllocator {
public:
char fixedBuffer_[Size];
FixedLinearAllocator()
: LinearAllocator(fixedBuffer_, Size) {
}
static const bool kNeedFree = false;
};
// wonder why this isn't a thing already, maybe I missed it
class DirectStringBuffer {
public:
using Ch = char;
char *buffer_;
char *end_;
char *current_;
DirectStringBuffer(char *buffer, size_t maxLen)
: buffer_(buffer), end_(buffer + maxLen), current_(buffer) {
}
void Put(char c) {
if (current_ < end_) {
*current_++ = c;
}
}
void Flush() {}
size_t GetSize() const { return static_cast<size_t>(current_ - buffer_); }
};
using MallocAllocator = rapidjson::CrtAllocator;
using PoolAllocator = rapidjson::MemoryPoolAllocator<MallocAllocator>;
using UTF8 = rapidjson::UTF8<char>;
// Writer appears to need about 16 bytes per nested object level (with 64bit size_t)
using StackAllocator = FixedLinearAllocator<2048>;
constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t));
using JsonWriterBase =
rapidjson::Writer<DirectStringBuffer, UTF8, UTF8, StackAllocator, rapidjson::kWriteNoFlags>;
class JsonWriter : public JsonWriterBase {
public:
DirectStringBuffer stringBuffer_;
StackAllocator stackAlloc_;
JsonWriter(char *dest, size_t maxLen)
: JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels), stringBuffer_(dest, maxLen), stackAlloc_() {
}
size_t Size() const { return stringBuffer_.GetSize(); }
};
using JsonDocumentBase = rapidjson::GenericDocument<UTF8, PoolAllocator, StackAllocator>;
class JsonDocument : public JsonDocumentBase {
public:
static const int kDefaultChunkCapacity = 32 * 1024;
// json parser will use this buffer first, then allocate more if needed; I seriously doubt we
// send any messages that would use all of this, though.
char parseBuffer_[32 * 1024];
MallocAllocator mallocAllocator_;
PoolAllocator poolAllocator_;
StackAllocator stackAllocator_;
JsonDocument()
: JsonDocumentBase(rapidjson::kObjectType,
&poolAllocator_,
sizeof(stackAllocator_.fixedBuffer_),
&stackAllocator_),
poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_), stackAllocator_() {
}
};
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsObject()) {
return &member->value;
}
}
return nullptr;
}
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsInt()) {
return member->value.GetInt();
}
}
return notFoundDefault;
}
inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsString()) {
return member->value.GetString();
}
}
return notFoundDefault;
}
} // namespace discord_rpc
#endif // DISCORD_SERIALIZATION_H

View File

@@ -208,26 +208,15 @@ else()
pkg_check_modules(TAGLIB REQUIRED IMPORTED_TARGET taglib>=1.12)
endif()
find_package(projectM4 COMPONENTS Playlist)
if(projectM4_FOUND)
set(LIBPROJECTM_FOUND ON)
set(HAVE_PROJECTM4 ON)
set(LIBPROJECTM_LIBRARIES libprojectM::projectM libprojectM::playlist)
else()
pkg_check_modules(LIBPROJECTM libprojectM)
endif()
find_package(GTest)
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
find_package(RapidJSON)
set(QT_VERSION_MAJOR 6)
set(QT_MIN_VERSION 6.4.0)
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
set(QT_COMPONENTS Core Concurrent Gui Widgets Network Sql)
set(QT_OPTIONAL_COMPONENTS GuiPrivate OpenGLWidgets LinguistTools Test)
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test)
if(UNIX AND NOT APPLE)
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
endif()
@@ -268,18 +257,7 @@ if(APPLE)
endif()
if(WIN32)
find_package(getopt NAMES getopt getopt-win unofficial-getopt-win32 REQUIRED)
if(TARGET getopt::getopt)
set(GETOPT_LIBRARIES getopt::getopt)
elseif(TARGET getopt-win::getopt)
set(GETOPT_LIBRARIES getopt-win::getopt)
elseif(TARGET getopt::getopt_shared)
set(GETOPT_LIBRARIES getopt::getopt_shared)
elseif(TARGET unofficial::getopt-win32::getopt)
set(GETOPT_LIBRARIES unofficial::getopt-win32::getopt)
else()
message(FATAL_ERROR "Missing getopt")
endif()
find_package(getopt-win REQUIRED)
endif()
if(APPLE OR WIN32)
@@ -386,9 +364,7 @@ optional_component(STREAMTAGREADER ON "Stream tagreader"
DEPENDS "sparsehash" LIBSPARSEHASH_FOUND
)
optional_component(DISCORD_RPC ON "Discord Rich Presence"
DEPENDS "RapidJSON" RapidJSON_FOUND
)
optional_component(DISCORD_RPC ON "Discord Rich Presence")
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
set(HAVE_CHROMAPRINT ON)
@@ -398,11 +374,6 @@ if(HAVE_X11_GLOBALSHORTCUTS OR HAVE_KGLOBALACCEL_GLOBALSHORTCUTS OR APPLE OR WIN
set(HAVE_GLOBALSHORTCUTS ON)
endif()
optional_component(VISUALIZATIONS ON "Visualizations"
DEPENDS "libprojectm" LIBPROJECTM_FOUND
DEPENDS "QtOpenGLWidgets" Qt${QT_VERSION_MAJOR}OpenGLWidgets_FOUND
)
if(NOT CMAKE_CROSSCOMPILING)
# Check that we have Qt with sqlite driver
set(CMAKE_REQUIRED_FLAGS "-std=c++17")
@@ -715,7 +686,6 @@ set(SOURCES
src/lyrics/elyricsnetlyricsprovider.cpp
src/lyrics/letraslyricsprovider.cpp
src/lyrics/lyricfindlyricsprovider.cpp
src/lyrics/lrcliblyricsprovider.cpp
src/settings/settingsdialog.cpp
src/settings/settingspage.cpp
@@ -809,7 +779,9 @@ set(SOURCES
src/scrobbler/scrobblercache.cpp
src/scrobbler/scrobblercacheitem.cpp
src/scrobbler/scrobblemetadata.cpp
src/scrobbler/scrobblingapi20.cpp
src/scrobbler/lastfmscrobbler.cpp
src/scrobbler/librefmscrobbler.cpp
src/scrobbler/listenbrainzscrobbler.cpp
src/scrobbler/lastfmimport.cpp
@@ -1012,7 +984,6 @@ set(HEADERS
src/lyrics/elyricsnetlyricsprovider.h
src/lyrics/letraslyricsprovider.h
src/lyrics/lyricfindlyricsprovider.h
src/lyrics/lrcliblyricsprovider.h
src/settings/settingsdialog.h
src/settings/settingspage.h
@@ -1101,7 +1072,9 @@ set(HEADERS
src/scrobbler/scrobblersettingsservice.h
src/scrobbler/scrobblerservice.h
src/scrobbler/scrobblercache.h
src/scrobbler/scrobblingapi20.h
src/scrobbler/lastfmscrobbler.h
src/scrobbler/librefmscrobbler.h
src/scrobbler/listenbrainzscrobbler.h
src/scrobbler/lastfmimport.h
@@ -1477,7 +1450,6 @@ optional_source(HAVE_QOBUZ
src/qobuz/qobuzrequest.cpp
src/qobuz/qobuzstreamurlrequest.cpp
src/qobuz/qobuzfavoriterequest.cpp
src/qobuz/qobuzcredentialfetcher.cpp
src/settings/qobuzsettingspage.cpp
src/covermanager/qobuzcoverprovider.cpp
HEADERS
@@ -1487,33 +1459,12 @@ optional_source(HAVE_QOBUZ
src/qobuz/qobuzrequest.h
src/qobuz/qobuzstreamurlrequest.h
src/qobuz/qobuzfavoriterequest.h
src/qobuz/qobuzcredentialfetcher.h
src/settings/qobuzsettingspage.h
src/covermanager/qobuzcoverprovider.h
UI
src/settings/qobuzsettingspage.ui
)
optional_source(HAVE_VISUALIZATIONS
SOURCES
src/visualizations/projectmpresetmodel.cpp
src/visualizations/projectmvisualization.cpp
src/visualizations/visualizationcontainer.cpp
src/visualizations/visualizationoverlay.cpp
src/visualizations/visualizationselector.cpp
src/visualizations/visualizationopenglwidget.cpp
HEADERS
src/visualizations/projectmpresetmodel.h
src/visualizations/projectmvisualization.h
src/visualizations/visualizationcontainer.h
src/visualizations/visualizationoverlay.h
src/visualizations/visualizationselector.h
src/visualizations/visualizationopenglwidget.h
UI
src/visualizations/visualizationoverlay.ui
src/visualizations/visualizationselector.ui
)
qt_wrap_cpp(SOURCES ${HEADERS})
qt_wrap_ui(SOURCES ${UI})
qt_add_resources(SOURCES data/data.qrc data/icons.qrc)
@@ -1584,7 +1535,6 @@ target_link_libraries(strawberry_lib PUBLIC
Qt${QT_VERSION_MAJOR}::Sql
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
$<$<BOOL:${HAVE_VISUALIZATIONS}>:Qt${QT_VERSION_MAJOR}::OpenGLWidgets>
ICU::uc
ICU::i18n
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>
@@ -1600,11 +1550,9 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
$<$<BOOL:${HAVE_VISUALIZATIONS}>:${LIBPROJECTM_LIBRARIES}>
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
$<$<BOOL:${WIN32}>:dsound dwmapi getopt-win::getopt>
$<$<BOOL:${MSVC}>:WindowsApp>
KDAB::kdsingleapplication
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
)
if(APPLE)
@@ -1623,6 +1571,10 @@ if(APPLE)
endif()
endif()
if(HAVE_DISCORD_RPC)
target_link_libraries(strawberry_lib PRIVATE discord-rpc)
endif()
target_link_libraries(strawberry PUBLIC strawberry_lib)
if(NOT APPLE)

105
Changelog
View File

@@ -2,111 +2,6 @@ Strawberry Music Player
=======================
ChangeLog
Version 1.2.16 (2025.12.16):
* Make Discord Rich presence use filename if song title is missing
* Added better error message when a GStreamer plugin is missing
* Preserve track order in album shuffle mode when restarting playback (#1623)
* Possible fixes for context word wrap
* Added lyrics from lrclib.net
* Added option to turn off the use of sort tags for the collection
* Fixed Spotify login
* Fixed error dialog shown minimized if another Strawberry window than the mainwindow was active
* Fixed seeking to the end of the track and back causing seeking to stop working (#1675)
* Set current index when automatically selecting track (#1825)
* Make icon size for shuffle and repeat buttons adjust to screen resolution (#1838)
* Fixed song being removed from playlist when dragging to another application (#1815)
* Don't automatically scroll on dynamic playlists (#1427)
Version 1.2.15 (2025.11.25):
* Fixed system default language not respected
* Fixed length filter search
* Fixed playlist parser converting Spotify URL's
* Removed use of deprecated QStyle::State_Editing
* Ignore connection closed errors for ListenBrainz
* (Windows) Support building with vcpkg unofficial::getopt-win32::getopt
Version 1.2.14 (2025.10.25):
Bugfixes:
* Fixed showing error dialog minimized when main window is not current active window (#1739)
* Fixed Discord timestamp update when seeking (#1813)
* Fixed CD metadata lookup to respect MusicBrainz rate limiting
* Fixed Tidal Open API cover provider
* (Windows) Fixed device selection with WASAPI2
Enhancements/Other:
* Removed libre.fm support
* Rewrote MusicBrainzClient to use Json instead of XML
* Subsonic will now use cover art from album when available
* Added option to remove "Remastered", etc from song titles for Tidal, Qobuz and Spotify
* Added webm to supported file extensions
* (Windows|MinGW) Added WASAPI2 support
* (Windows) Added experimental exclusive mode for WASAPI2
Version 1.2.13 (2025.08.31):
Bugfixes:
* Fixed playlist alternating row colors no longer working with some styles (#1806)
* Fixed "Open Audio CD" no longer working (#1803)
* Fixed systemtray icon playback status not working with scaling (#1782)
* Fixed build without MusicBrainz (#1799)
* Fixed build without MTP (#1804)
Enhancements:
* Added Discord status text option (#1796)
* Read Vorbis/FLAC "Other" embedded covers if front cover is not available (#1793)
Version 1.2.12 (2025.08.12):
Bugfixes:
* Fixed scrobbling for radio streams.
* Fixed CDDA memory leaks.
* Fixed device view CDDA loading (#1676).
* Fixed collection directory editing (#1767).
* Fixed devices sometimes being duplicated in the database.
* Fixed alternating playlist row colors with Windows 11 style.
* Fixed broken file filter for GME formats.
* Fixed collection scanning for GME formats.
* Fixed Chartlyrics.
* Fixed network cache file descriptor leak on lyrics search with workaround for QTBUG-135641.
* Fixed parsing Tidal urls with certain stream URL replies.
* Fixed pixelated window icon on Wayland (#1753).
* Fixed saving collection grouping with special characters in the name (#1758).
* Fixed Spotify token not automatically updated on renewal when playing (#1769).
* (macOS/Windows) Fixed network cache file descriptor leak with patch for QTBUG-135641.
* (Windows|MSVC) Fixed installer to not restart the computer after installing Visual C++ Redistributable.
Enhancements:
* Implemented edit tag dialog reset for year, track, disc and rating.
* Added ALAC to supported filetypes for iPods.
* Added CD-TEXT support.
* Added back Genius lyrics.
* Added support for reporting more info to ListenBrainz.
* Added support for BPM, mood and initial key tags.
* Added support for sort tags to collection, playlists and smart playlists.
Version 1.2.11 (2025.05.15):
* Fixed playlist songs sometimes not updated with new cover.
* Fixed context album cover showing even when it's disabled in the setting (#1744).
* Fixed crash when dragging songs to a closed playlist (#1741).
* Enable startup notify in desktop file.
* (Windows|MSVC) Add experimental support for native ARM64 builds.
* (Windows|MinGW) Fixed crash on exit.
Version 1.2.10 (2025.04.18):
Bugfixes:
* Fixed Discord rich presence showing bogus artist and album.
* Fixed incorrect ID3v2 comment tag.
* (macOS|Windows MSVC) Fixed stuck playback of some streams.
Enhancements:
* Removed Genius lyrics (longer working properly because of website changes).
* (macOS|Windows MSVC) Added back Spotify
Version 1.2.9 (2025.04.08):
Bugfixes:

170
README.md
View File

@@ -1,137 +1,117 @@
# :strawberry: Strawberry Music Player [![Build Status](https://github.com/strawberrymusicplayer/strawberry/workflows/Build/badge.svg)](https://github.com/strawberrymusicplayer/strawberry/actions)
:strawberry: Strawberry Music Player [![Build Status](https://github.com/strawberrymusicplayer/strawberry/workflows/Build/badge.svg)](https://github.com/strawberrymusicplayer/strawberry/actions)
=======================
[![Sponsor](https://img.shields.io/badge/-Sponsor-green?logo=github)](https://github.com/sponsors/jonaski)
[![Patreon](https://img.shields.io/badge/patreon-donate-green.svg)](https://patreon.com/jonaskvinge)
[![PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/jonaskvinge)
Strawberry is a **music player and music collection organizer**, originally forked from *Clementine* in 2018.
Its written in **C++ using the Qt framework**, designed for **audiophiles and music collectors**.
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt framework.
![Screenshot of Strawberry Music Player](https://raw.githubusercontent.com/strawberrymusicplayer/strawberry/master/data/screenshot/screenshot.png)
![Browse](https://raw.githubusercontent.com/strawberrymusicplayer/strawberry/master/data/screenshot/screenshot.png)
---
Resources:
## :globe_with_meridians: Resources
* Website: https://www.strawberrymusicplayer.org/
* Wiki: https://wiki.strawberrymusicplayer.org/
* Forum: https://forum.strawberrymusicplayer.org/
* Github: https://github.com/strawberrymusicplayer/strawberry
* Latest builds: https://builds.strawberrymusicplayer.org/
* openSUSE buildservice: https://build.opensuse.org/package/show/home:jonaski:audio/strawberry
* Ubuntu PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
* Ubuntu Unstable PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
* Translations: https://crowdin.com/project/strawberrymusicplayer/
- **Website:** https://www.strawberrymusicplayer.org
- **Wiki:** https://wiki.strawberrymusicplayer.org
- **Forum:** https://forum.strawberrymusicplayer.org
- **GitHub:** https://github.com/strawberrymusicplayer/strawberry
- **Latest builds:** https://builds.strawberrymusicplayer.org
- **openSUSE Build Service:**
- Stable: https://build.opensuse.org/package/show/home:jonaski:strawberry/strawberry
- Unstable: https://build.opensuse.org/package/show/home:jonaski:strawberry-dev/strawberry
- **Ubuntu PPAs:**
- Stable: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
- Unstable: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
- **Translations:** https://crowdin.com/project/strawberrymusicplayer
### :bangbang: Opening an issue
---
* Read the FAQ: https://wiki.strawberrymusicplayer.org/wiki/FAQ
* Search for the issue to see if it is already solved, or if there is an open issue for it already. If there is an open issue already, you can comment on it if you have additional information that could be useful to us.
* For technical problems, discussion, questions and feature suggestions use the forum (https://forum.strawberrymusicplayer.org/) instead. The forum is better suited for discussion.
* We do not take feature requests from users on GitHub. Any issues related to feature requests will be closed. This does not necessarily mean that we won't add new features, but we don't have time to take feature requests or answer questions about new features from users. It is still possible to suggest or discuss new features on the forum (https://forum.strawberrymusicplayer.org/).
* We do not maintain the Flatpak package. Do not report issues related to Flatpak unless the issue can be reproduced with a native package, use Flatpak support instead https://flatpak.org/about/
## :warning: Opening an Issue
### :moneybag: Sponsoring
Before creating a new GitHub issue:
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. **Read the [FAQ](https://wiki.strawberrymusicplayer.org/wiki/FAQ)**.
2. **Search existing issues** to avoid duplicates. If one already exists, comment there with any additional information.
3. **Use the [forum](https://forum.strawberrymusicplayer.org/)** for technical problems, discussions or feature suggestions — its better suited for back-and-forth conversation.
4. **Feature requests are not accepted on GitHub.** Issues created for feature requests will be closed. You can still discuss ideas on the forum.
5. **Flatpak users:** We do **not** maintain the Flatpak package. Report Flatpak-specific issues via [Flatpak support](https://flatpak.org/about/).
---
## :moneybag: Sponsoring
Strawberry is **free software released under the GPL**.
If you enjoy using it, please consider **supporting development** through sponsorship or donation.
**Sponsorship options:**
1. [Patreon](https://www.patreon.com/jonaskvinge)
2. [GitHub](https://github.com/sponsors/jonaski)
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
4. [PayPal](https://paypal.me/jonaskvinge)
Supporting open-source developers helps ensure continued maintenance and improvements.
Funding developers is a way to contribute to open source projects you appreciate, it helps developers get the resources they need, and recognize contributors working behind the scenes to make open source better for everyone.
---
### :heavy_check_mark: Features
## :white_check_mark: Features
* Play and organize music
* Supports WAV, FLAC, WavPack, Ogg FLAC, Ogg Vorbis, Ogg Opus, Ogg Speex, MPC, TrueAudio, AIFF, MP4, MP3, ASF and Monkey's Audio.
* Audio CD playback
* Native desktop notifications
* Playlist management
* Smart and dynamic playlists
* Advanced audio output and device configuration for bit-perfect playback on Linux
* In-player song loudness analysis and song playback loudness normalization, as per EBU R 128
* Edit tags on audio files
* Fetch tags from MusicBrainz
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
* Song lyrics from [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/)
* Support for multiple backends
* Audio analyzer
* Audio equalizer
* Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
* Streaming from Subsonic compatible servers
* Unofficial Tidal, Spotify and Qobuz integration
* Discord rich presence
- Play and organize your music collection
- Supports formats: WAV, FLAC, WavPack, Ogg Vorbis, Opus, MPC, TrueAudio, AIFF, MP4, MP3, ASF, and Monkeys Audio
- Audio CD playback
- Bit-perfect playback on Linux
- Native desktop notifications
- Advanced playlist management
- Smart and dynamic playlists
- Loudness analysis and EBU R128 normalization
- Editing tags and fetching missing tags via [MusicBrainz](https://musicbrainz.org/)
- Album art from: [Last.fm](https://www.last.fm/), [MusicBrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/), [Spotify](https://www.spotify.com/)
- Lyrics from: [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics](https://www.lololyrics.com/), [songlyrics](https://www.songlyrics.com/), [azlyrics](https://www.azlyrics.com/), [elyrics](https://www.elyrics.net/), [letras](https://www.letras.mus.br), [LyricFind](https://lyrics.lyricfind.com) and [lrclib.net](https://lrclib.net/)
- Audio analyzer and equalizer
- Transfer music to USB, MTP and iPod devices
- Scrobbling to [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/)
- Streaming from Subsonic-compatible servers
- Unofficial integrations: Tidal, Spotify, and Qobuz
- Discord Rich Presence
---
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
:white_check_mark: Tested on **Linux**, **OpenBSD**, **FreeBSD**, **macOS**, and **Windows**.
**Access to macOS and Windows releases are currently restricted to sponsors, a 5 USD monthly sponsorship is required. You can sponsor strawberry through <a href="https://www.patreon.com/jonaskvinge">Patreon</a> for direct access to new releases. If you are sponsoring through GitHub, Ko-fi or PayPal, please e-mail support AT strawberrymusicplayer.org for access to downloads.**
> **Note:** macOS and Windows releases are currently **available to sponsors only**.
> A monthly sponsorship via [Patreon](https://www.patreon.com/jonaskvinge) grants direct access to new releases.
### :heavy_exclamation_mark: Requirements
---
To build Strawberry from source you need the following installed on your system with the additional development packages/headers:
## :gear: Requirements
* [CMake 3.13 or higher](https://cmake.org/)
* C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/) or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
* [Boost](https://www.boost.org/)
* [GLib](https://developer.gnome.org/glib/)
* [Qt 6.4.0 or higher with components Core, Concurrent, Gui, Widgets, Network, Sql and D-Bus](https://www.qt.io/)
* [SQLite 3.9 or newer](https://www.sqlite.org)
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
* [GStreamer](https://gstreamer.freedesktop.org/)
* [TagLib 1.12 or higher](https://www.taglib.org/)
* [ICU](https://unicode-org.github.io/icu/)
* [KDSingleApplication 1.1.0 or higher](https://github.com/KDAB/KDSingleApplication)
To build Strawberry from source, youll need:
Optional dependencies:
**Dependencies:**
- [CMake ≥= 3.13](https://cmake.org/)
- C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/), or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
- [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
- [Boost](https://www.boost.org/)
- [GLib](https://developer.gnome.org/glib/)
- [Qt ≥= 6.4](https://www.qt.io/) (Core, Concurrent, Gui, Widgets, Network, SQL, D-Bus)
- [SQLite ≥= 3.9](https://www.sqlite.org)
- [ALSA (Linux only)](https://www.alsa-project.org/)
- [GStreamer](https://gstreamer.freedesktop.org/)
- [TagLib ≥= 1.12](https://www.taglib.org/)
- [ICU](https://unicode-org.github.io/icu/)
- [KDSingleApplication ≥= 1.1.0](https://github.com/KDAB/KDSingleApplication)
* Song fingerprinting and MusicBrainz tagging: [Chromaprint](https://acoustid.org/chromaprint)
* Moodbar: [fftw3](http://www.fftw.org/)
* PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/?)
* Audio CD: [libcdio](https://www.gnu.org/software/libcdio/)
* MTP devices: [libmtp](http://libmtp.sourceforge.net/)
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
**Dependencies for optional features:**
- Fingerprinting & tagging: [Chromaprint](https://acoustid.org/chromaprint)
- Moodbar: [FFTW3](http://www.fftw.org/)
- PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/)
- Audio CD support: [libcdio](https://www.gnu.org/software/libcdio/)
- MTP devices: [libmtp](http://libmtp.sourceforge.net/)
- iPod Classic: [libgpod](http://www.gtkpod.org/libgpod/)
- EBU R128 normalization: [libebur128](https://github.com/jiixyj/libebur128)
- Discord presence: [RapidJSON](https://rapidjson.org/)
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.
Also install GStreamer plugins **base**, **good**, and optionally **bad**, **ugly** and **libav** for full codec support.
### :wrench: Build from source
---
## :wrench: Build from Source
**Get the code:**
### Get the code:
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
**Build and install:**
### Build and install:
cd strawberry
cmake -S . -B build
cmake --build build --parallel $(nproc)
sudo cmake --install build
For building on Windows with Visual Studio 2022, see: :point_right: https://github.com/strawberrymusicplayer/strawberry-msvc
To build on Windows with Visual Studio 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
---
## :package: Packaging status
### :penguin: Packaging status
[![Packaging status](https://repology.org/badge/vertical-allrepos/strawberry.svg?columns=3&header=Strawberry&exclude_unsupported=1)](https://repology.org/metapackage/strawberry/versions)

View File

@@ -1,6 +1,6 @@
set(STRAWBERRY_VERSION_MAJOR 1)
set(STRAWBERRY_VERSION_MINOR 2)
set(STRAWBERRY_VERSION_PATCH 16)
set(STRAWBERRY_VERSION_PATCH 9)
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION ON)

View File

@@ -12,7 +12,6 @@
<file>schema/schema-18.sql</file>
<file>schema/schema-19.sql</file>
<file>schema/schema-20.sql</file>
<file>schema/schema-21.sql</file>
<file>schema/device-schema.sql</file>
<file>style/strawberry.css</file>
<file>style/smartplaylistsearchterm.css</file>

View File

@@ -12,13 +12,9 @@ CREATE TABLE device_%deviceid_subdirectories (
CREATE TABLE device_%deviceid_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -26,9 +22,7 @@ CREATE TABLE device_%deviceid_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -92,11 +86,7 @@ CREATE TABLE device_%deviceid_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
@@ -104,4 +94,4 @@ CREATE INDEX idx_device_%deviceid_songs_album ON device_%deviceid_songs (album);
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
UPDATE devices SET schema_version=6 WHERE ROWID=%deviceid;
UPDATE devices SET schema_version=5 WHERE ROWID=%deviceid;

View File

@@ -1,43 +0,0 @@
DROP INDEX IF EXISTS idx_albumartistsort;
DROP INDEX IF EXISTS idx_albumsort;
DROP INDEX IF EXISTS idx_artistsort;
DROP INDEX IF EXISTS idx_composersort;
DROP INDEX IF EXISTS idx_performersort;
DROP INDEX IF EXISTS idx_titlesort;
ALTER TABLE %allsongstables ADD COLUMN albumartistsort TEXT;
ALTER TABLE %allsongstables ADD COLUMN albumsort TEXT;
ALTER TABLE %allsongstables ADD COLUMN artistsort TEXT;
ALTER TABLE %allsongstables ADD COLUMN composersort TEXT;
ALTER TABLE %allsongstables ADD COLUMN performersort TEXT;
ALTER TABLE %allsongstables ADD COLUMN titlesort TEXT;
ALTER TABLE %allsongstables ADD COLUMN bpm REAL;
ALTER TABLE %allsongstables ADD COLUMN mood TEXT;
ALTER TABLE %allsongstables ADD COLUMN initial_key TEXT;
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
UPDATE schema_version SET version=21;

View File

@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
DELETE FROM schema_version;
INSERT INTO schema_version (version) VALUES (21);
INSERT INTO schema_version (version) VALUES (20);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
@@ -20,13 +20,9 @@ CREATE TABLE IF NOT EXISTS subdirectories (
CREATE TABLE IF NOT EXISTS songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -34,9 +30,7 @@ CREATE TABLE IF NOT EXISTS songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -100,24 +94,16 @@ CREATE TABLE IF NOT EXISTS songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS subsonic_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -125,9 +111,7 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -191,24 +175,16 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -216,9 +192,7 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -282,24 +256,16 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -307,9 +273,7 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -373,24 +337,16 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS tidal_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -398,9 +354,7 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -464,24 +418,16 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -489,9 +435,7 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -555,24 +499,16 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -580,9 +516,7 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -646,24 +580,16 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS spotify_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -671,9 +597,7 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -737,24 +661,16 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -762,9 +678,7 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -828,24 +742,16 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -853,9 +759,7 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -919,24 +823,16 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
CREATE TABLE IF NOT EXISTS qobuz_songs (
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
@@ -944,9 +840,7 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -1010,11 +904,7 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
@@ -1041,13 +931,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
playlist_url TEXT,
title TEXT,
titlesort TEXT,
album TEXT,
albumsort TEXT,
artist TEXT,
artistsort TEXT,
albumartist TEXT,
albumartistsort TEXT,
track INTEGER,
disc INTEGER,
year INTEGER,
@@ -1055,9 +941,7 @@ CREATE TABLE IF NOT EXISTS playlist_items (
genre TEXT,
compilation INTEGER DEFAULT 0,
composer TEXT,
composersort TEXT,
performer TEXT,
performersort TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
@@ -1121,11 +1005,7 @@ CREATE TABLE IF NOT EXISTS playlist_items (
musicbrainz_work_id TEXT,
ebur128_integrated_loudness_lufs REAL,
ebur128_loudness_range_lu REAL,
bpm REAL,
mood TEXT,
initial_key TEXT
ebur128_loudness_range_lu REAL
);
@@ -1152,22 +1032,10 @@ CREATE INDEX IF NOT EXISTS idx_comp_artist ON songs (compilation_effective, arti
CREATE INDEX IF NOT EXISTS idx_albumartist ON songs (albumartist);
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
CREATE INDEX IF NOT EXISTS idx_artist ON songs (artist);
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
CREATE VIEW IF NOT EXISTS duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;

7
debian/control vendored
View File

@@ -32,8 +32,7 @@ Build-Depends: debhelper-compat (= 12),
libfftw3-dev,
libebur128-dev,
libsparsehash-dev,
rapidjson-dev,
libprojectm-dev
rapidjson-dev
Standards-Version: 4.7.0
Package: strawberry
@@ -61,11 +60,11 @@ Description: music player and music collection organizer
- Edit tags on audio files
- Automatically retrieve tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
- Lyrics from multiple sources
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
- Audio analyzer
- Audio equalizer
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
- Scrobbler with support for Last.fm and ListenBrainz
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
- Streaming support for Subsonic-compatible servers
- Unofficial streaming support for Tidal and Qobuz
.

22
dist/macos/macversion.sh vendored Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
macos_version=$(sw_vers -productVersion)
macos_version_major=$(echo $macos_version | awk -F '[.]' '{print $1}')
macos_version_minor=$(echo $macos_version | awk -F '[.]' '{print $2}')
if [ "${macos_version_major}" = "10" ]; then
macos_codenames=(
["13"]="highsierra"
["14"]="mojave"
["15"]="catalina"
)
if [[ -n "${macos_codenames[$macos_version_minor]}" ]]; then
echo "${macos_codenames[$macos_version_minor]}"
else
echo "unknown"
fi
elif [ "${macos_version_major}" = "11" ]; then
echo "bigsur"
else
echo "unknown"
fi

View File

@@ -31,10 +31,10 @@
<li>Edit tags on audio files</li>
<li>Automatically retrieve tags from MusicBrainz</li>
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
<li>Lyrics from multiple sources</li>
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</li>
<li>Audio analyzer and equalizer</li>
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
<li>Scrobbler with support for Last.fm and ListenBrainz</li>
<li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li>
<li>Streaming support for Subsonic-compatible servers</li>
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
</ul>
@@ -51,13 +51,6 @@
</screenshots>
<update_contact>eclipseo@fedoraproject.org</update_contact>
<releases>
<release version="1.2.16" date="2025-12-16"/>
<release version="1.2.15" date="2025-11-25"/>
<release version="1.2.14" date="2025-10-25"/>
<release version="1.2.13" date="2025-08-31"/>
<release version="1.2.12" date="2025-08-12"/>
<release version="1.2.11" date="2025-05-15"/>
<release version="1.2.10" date="2025-04-18"/>
<release version="1.2.9" date="2025-04-08"/>
<release version="1.2.8" date="2025-04-05"/>
<release version="1.2.7" date="2025-01-31"/>

View File

@@ -13,7 +13,8 @@ TryExec=strawberry
Icon=strawberry
Terminal=false
Categories=AudioVideo;Player;Qt;Audio;
Keywords=Audio;Player;Clementine;
Keywords=Audio;Player;
StartupNotify=false
MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
StartupWMClass=strawberry
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;

View File

@@ -29,7 +29,9 @@ Features:
.br
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
.br
- Lyrics from multiple sources
- Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
.br
- Support for multiple backends
.br
- Audio analyzer
.br
@@ -37,7 +39,7 @@ Features:
.br
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
.br
- Scrobbler with support for Last.fm and ListenBrainz
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
.br
- Streaming support for Subsonic-compatible servers
.br

View File

@@ -43,7 +43,6 @@ BuildRequires: pkgconfig(taglib)
BuildRequires: pkgconfig(fftw3)
BuildRequires: pkgconfig(icu-uc)
BuildRequires: pkgconfig(icu-i18n)
BuildRequires: pkgconfig(libprojectM)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Core)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Concurrent)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Network)
@@ -94,15 +93,16 @@ Features:
- Edit tags on audio files
- Automatically retrieve tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
- Lyrics from multiple sources
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
- Support for multiple backends
- Audio analyzer
- Audio equalizer
- Scrobbler with support for Last.fm and ListenBrainz
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
- Streaming support for Subsonic-compatible servers
- Unofficial streaming support for Tidal and Qobuz
%if 0%{?suse_version} && 0%{?suse_version} < 1600
%if 0%{?suse_version} && 0%{?suse_version} <= 1600
%debug_package
%endif

View File

@@ -21,10 +21,6 @@
!define arch_x64
!else if "@ARCH@" == "x86_64-w64-mingw32.shared"
!define arch_x64
!else if "@ARCH@" == "arm64"
!define arch_arm64
!else
!error "Missing ARCH"
!endif
!ifdef arch_x86
@@ -35,10 +31,6 @@
!define arch "x64"
!endif
!ifdef arch_arm64
!define arch "arm64"
!endif
!if "@CMAKE_BUILD_TYPE@" == "Release"
!define release
@@ -46,8 +38,6 @@
!define release
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
!define debug
!else
!error "Missing CMAKE_BUILD_TYPE"
!endif
!ifdef release
@@ -80,7 +70,7 @@
!ifdef arch_x86
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug"
!endif
!ifdef arch_x64 || arch_arm64
!ifdef arch_x64
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug"
!endif
!else
@@ -90,7 +80,7 @@
!ifdef arch_x86
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player"
!endif
!ifdef arch_x64 || arch_arm64
!ifdef arch_x64
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player"
!endif
!endif
@@ -224,7 +214,7 @@ Function InstallMSVCRuntime
; ${If} $R0 == ""
SetDetailsView hide
; 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 /norestart'
ExecWait '"$TEMP\${vc_redist_file}" /install /passive'
Delete "$TEMP\${vc_redist_file}"
SetDetailsView show
; ${EndIf}
@@ -334,7 +324,7 @@ Section "Strawberry" Strawberry
File "libqtsparkle-qt6.dll"
File "libsoup-3.0-0.dll"
File "libspeex-1.dll"
File "libsqlite3-0.dll"
File "libsqlite3.dll"
File "libssp-0.dll"
File "libstdc++-6.dll"
File "libtag.dll"
@@ -377,10 +367,6 @@ Section "Strawberry" Strawberry
File "libcrypto-3-x64.dll"
File "libssl-3-x64.dll"
!endif
!ifdef arch_arm64
File "libcrypto-3-arm64.dll"
File "libssl-3-arm64.dll"
!endif
File "FLAC.dll"
File "brotlicommon.dll"
@@ -395,6 +381,7 @@ Section "Strawberry" Strawberry
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"
@@ -415,11 +402,14 @@ Section "Strawberry" Strawberry
File "gsttag-1.0-0.dll"
File "gsturidownloader-1.0-0.dll"
File "gstvideo-1.0-0.dll"
File "gstwinrt-1.0-0.dll"
File "harfbuzz.dll"
File "intl-8.dll"
File "jpeg62.dll"
File "kdsingleapplication-qt6.dll"
File "libbs2b.dll"
File "libfaac_dll.dll"
File "liblzma.dll"
File "libmp3lame.dll"
File "libopenmpt.dll"
File "mpcdec.dll"
@@ -437,11 +427,6 @@ Section "Strawberry" Strawberry
File "vorbisfile.dll"
File "wavpackdll.dll"
!ifndef arch_arm64
File "gnutls.dll"
File "libfaac_dll.dll"
!endif
!ifdef release
File "freetype.dll"
File "libiconv.dll"
@@ -449,10 +434,8 @@ Section "Strawberry" Strawberry
File "libspeex.dll"
File "pcre2-8.dll"
File "pcre2-16.dll"
File "zlib1.dll"
!ifndef arch_arm64
File "twolame.dll"
!endif
File "zlib.dll"
!endif
!ifdef debug
File "freetyped.dll"
@@ -461,10 +444,8 @@ Section "Strawberry" Strawberry
File "libspeexd.dll"
File "pcre2-8d.dll"
File "pcre2-16d.dll"
File "zlibd1.dll"
!ifndef arch_arm64
File "twolamed.dll"
!endif
File "zlibd.dll"
!endif
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
@@ -477,15 +458,11 @@ Section "Strawberry" Strawberry
; Common files
File "icudt78.dll"
!ifdef msvc && arch_arm64
File "fftw3.dll"
!else
File "icudt77.dll"
File "libfftw3-3.dll"
!endif
!ifdef msvc && debug
File "icuin78d.dll"
File "icuuc78d.dll"
File "icuin77d.dll"
File "icuuc77d.dll"
File "libxml2d.dll"
File "Qt6Concurrentd.dll"
File "Qt6Cored.dll"
@@ -494,8 +471,8 @@ Section "Strawberry" Strawberry
File "Qt6Sqld.dll"
File "Qt6Widgetsd.dll"
!else
File "icuin78.dll"
File "icuuc78.dll"
File "icuin77.dll"
File "icuuc77.dll"
File "libxml2.dll"
File "Qt6Concurrent.dll"
File "Qt6Core.dll"
@@ -505,7 +482,6 @@ Section "Strawberry" Strawberry
File "Qt6Widgets.dll"
!endif
!ifdef msvc && arch_x86
File "avcodec-61.dll"
File "avfilter-10.dll"
File "avformat-61.dll"
@@ -513,14 +489,6 @@ Section "Strawberry" Strawberry
File "postproc-58.dll"
File "swresample-5.dll"
File "swscale-8.dll"
!else
File "avcodec-62.dll"
File "avfilter-11.dll"
File "avformat-62.dll"
File "avutil-60.dll"
File "swresample-6.dll"
File "swscale-9.dll"
!endif
; Register Strawberry with Default Programs
Var /GLOBAL AppIcon
@@ -558,13 +526,11 @@ Section "GIO modules" gio-modules
SetOutPath "$INSTDIR\gio-modules"
!ifdef mingw
File "/oname=libgiognutls.dll" "gio-modules\libgiognutls.dll"
File "/oname=libgioopenssl.dll" "gio-modules\libgioopenssl.dll"
!endif
!ifdef msvc
!ifdef arch_arm64
File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll"
!else
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
!endif
File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll"
!endif
SectionEnd
@@ -681,7 +647,6 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=libgstvolume.dll" "gstreamer-plugins\libgstvolume.dll"
File "/oname=libgstvorbis.dll" "gstreamer-plugins\libgstvorbis.dll"
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
File "/oname=libgstwasapi2.dll" "gstreamer-plugins\libgstwasapi2.dll"
File "/oname=libgstwaveform.dll" "gstreamer-plugins\libgstwaveform.dll"
File "/oname=libgstwavenc.dll" "gstreamer-plugins\libgstwavenc.dll"
File "/oname=libgstwavpack.dll" "gstreamer-plugins\libgstwavpack.dll"
@@ -709,6 +674,7 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=gstdirectsound.dll" "gstreamer-plugins\gstdirectsound.dll"
File "/oname=gstdsd.dll" "gstreamer-plugins\gstdsd.dll"
File "/oname=gstequalizer.dll" "gstreamer-plugins\gstequalizer.dll"
File "/oname=gstfaac.dll" "gstreamer-plugins\gstfaac.dll"
File "/oname=gstfaad.dll" "gstreamer-plugins\gstfaad.dll"
File "/oname=gstfdkaac.dll" "gstreamer-plugins\gstfdkaac.dll"
File "/oname=gstflac.dll" "gstreamer-plugins\gstflac.dll"
@@ -741,6 +707,7 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=gstspeex.dll" "gstreamer-plugins\gstspeex.dll"
File "/oname=gsttaglib.dll" "gstreamer-plugins\gsttaglib.dll"
File "/oname=gsttcp.dll" "gstreamer-plugins\gsttcp.dll"
File "/oname=gsttwolame.dll" "gstreamer-plugins\gsttwolame.dll"
File "/oname=gsttypefindfunctions.dll" "gstreamer-plugins\gsttypefindfunctions.dll"
File "/oname=gstudp.dll" "gstreamer-plugins\gstudp.dll"
File "/oname=gstvolume.dll" "gstreamer-plugins\gstvolume.dll"
@@ -752,12 +719,8 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=gstwavpack.dll" "gstreamer-plugins\gstwavpack.dll"
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
File "/oname=gstxingmux.dll" "gstreamer-plugins\gstxingmux.dll"
!ifndef arch_arm64
File "/oname=gstfaac.dll" "gstreamer-plugins\gstfaac.dll"
File "/oname=gsttwolame.dll" "gstreamer-plugins\gsttwolame.dll"
!endif
!ifdef arch_x64
File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
;File "/oname=gstspotify.dll" "gstreamer-plugins\gstspotify.dll"
!endif
!endif ; MSVC
@@ -886,7 +849,7 @@ Section "Uninstall"
Delete "$INSTDIR\libqtsparkle-qt6.dll"
Delete "$INSTDIR\libsoup-3.0-0.dll"
Delete "$INSTDIR\libspeex-1.dll"
Delete "$INSTDIR\libsqlite3-0.dll"
Delete "$INSTDIR\libsqlite3.dll"
Delete "$INSTDIR\libssp-0.dll"
Delete "$INSTDIR\libstdc++-6.dll"
Delete "$INSTDIR\libtag.dll"
@@ -929,10 +892,6 @@ Section "Uninstall"
Delete "$INSTDIR\libcrypto-3-x64.dll"
Delete "$INSTDIR\libssl-3-x64.dll"
!endif
!ifdef arch_arm64
Delete "$INSTDIR\libcrypto-3-arm64.dll"
Delete "$INSTDIR\libssl-3-arm64.dll"
!endif
Delete "$INSTDIR\FLAC.dll"
Delete "$INSTDIR\brotlicommon.dll"
@@ -947,6 +906,7 @@ Section "Uninstall"
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"
@@ -967,11 +927,14 @@ Section "Uninstall"
Delete "$INSTDIR\gsttag-1.0-0.dll"
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
Delete "$INSTDIR\gstvideo-1.0-0.dll"
Delete "$INSTDIR\gstwinrt-1.0-0.dll"
Delete "$INSTDIR\harfbuzz.dll"
Delete "$INSTDIR\intl-8.dll"
Delete "$INSTDIR\jpeg62.dll"
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
Delete "$INSTDIR\libbs2b.dll"
Delete "$INSTDIR\libfaac_dll.dll"
Delete "$INSTDIR\liblzma.dll"
Delete "$INSTDIR\libmp3lame.dll"
Delete "$INSTDIR\libopenmpt.dll"
Delete "$INSTDIR\mpcdec.dll"
@@ -989,11 +952,6 @@ Section "Uninstall"
Delete "$INSTDIR\vorbisfile.dll"
Delete "$INSTDIR\wavpackdll.dll"
!ifndef arch_arm64
Delete "$INSTDIR\gnutls.dll"
Delete "$INSTDIR\libfaac_dll.dll"
!endif
!ifdef release
Delete "$INSTDIR\freetype.dll"
Delete "$INSTDIR\libiconv.dll"
@@ -1001,10 +959,8 @@ Section "Uninstall"
Delete "$INSTDIR\libspeex.dll"
Delete "$INSTDIR\pcre2-8.dll"
Delete "$INSTDIR\pcre2-16.dll"
Delete "$INSTDIR\zlib1.dll"
!ifndef arch_arm64
Delete "$INSTDIR\twolame.dll"
!endif
Delete "$INSTDIR\zlib.dll"
!endif
!ifdef debug
Delete "$INSTDIR\freetyped.dll"
@@ -1013,10 +969,8 @@ Section "Uninstall"
Delete "$INSTDIR\libspeexd.dll"
Delete "$INSTDIR\pcre2-8d.dll"
Delete "$INSTDIR\pcre2-16d.dll"
Delete "$INSTDIR\zlibd1.dll"
!ifndef arch_arm64
Delete "$INSTDIR\twolamed.dll"
!endif
Delete "$INSTDIR\zlibd.dll"
!endif
!ifdef arch_x86
@@ -1028,15 +982,11 @@ Section "Uninstall"
; Common files
Delete "$INSTDIR\icudt78.dll"
!ifdef msvc && arch_arm64
Delete "$INSTDIR\fftw3.dll"
!else
Delete "$INSTDIR\icudt77.dll"
Delete "$INSTDIR\libfftw3-3.dll"
!endif
!ifdef msvc && debug
Delete "$INSTDIR\icuin78d.dll"
Delete "$INSTDIR\icuuc78d.dll"
Delete "$INSTDIR\icuin77d.dll"
Delete "$INSTDIR\icuuc77d.dll"
Delete "$INSTDIR\libxml2d.dll"
Delete "$INSTDIR\Qt6Concurrentd.dll"
Delete "$INSTDIR\Qt6Cored.dll"
@@ -1045,8 +995,8 @@ Section "Uninstall"
Delete "$INSTDIR\Qt6Sqld.dll"
Delete "$INSTDIR\Qt6Widgetsd.dll"
!else
Delete "$INSTDIR\icuin78.dll"
Delete "$INSTDIR\icuuc78.dll"
Delete "$INSTDIR\icuin77.dll"
Delete "$INSTDIR\icuuc77.dll"
Delete "$INSTDIR\libxml2.dll"
Delete "$INSTDIR\Qt6Concurrent.dll"
Delete "$INSTDIR\Qt6Core.dll"
@@ -1056,7 +1006,6 @@ Section "Uninstall"
Delete "$INSTDIR\Qt6Widgets.dll"
!endif
!ifdef msvc && arch_x86
Delete "$INSTDIR\avcodec-61.dll"
Delete "$INSTDIR\avfilter-10.dll"
Delete "$INSTDIR\avformat-61.dll"
@@ -1064,24 +1013,14 @@ Section "Uninstall"
Delete "$INSTDIR\postproc-58.dll"
Delete "$INSTDIR\swresample-5.dll"
Delete "$INSTDIR\swscale-8.dll"
!else
Delete "$INSTDIR\avcodec-62.dll"
Delete "$INSTDIR\avfilter-11.dll"
Delete "$INSTDIR\avformat-62.dll"
Delete "$INSTDIR\avutil-60.dll"
Delete "$INSTDIR\swresample-6.dll"
Delete "$INSTDIR\swscale-9.dll"
!endif
!ifdef mingw
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
Delete "$INSTDIR\gio-modules\libgioopenssl.dll"
!endif
!ifdef msvc
!ifdef arch_arm64
Delete "$INSTDIR\gio-modules\gioopenssl.dll"
!else
Delete "$INSTDIR\gio-modules\giognutls.dll"
!endif
Delete "$INSTDIR\gio-modules\gioopenssl.dll"
!endif
!ifdef msvc && debug
@@ -1165,7 +1104,6 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\libgstvolume.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstvorbis.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi2.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstwaveform.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstwavenc.dll"
Delete "$INSTDIR\gstreamer-plugins\libgstwavpack.dll"
@@ -1195,6 +1133,7 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstdirectsound.dll"
Delete "$INSTDIR\gstreamer-plugins\gstdsd.dll"
Delete "$INSTDIR\gstreamer-plugins\gstequalizer.dll"
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
Delete "$INSTDIR\gstreamer-plugins\gstfaad.dll"
Delete "$INSTDIR\gstreamer-plugins\gstfdkaac.dll"
Delete "$INSTDIR\gstreamer-plugins\gstflac.dll"
@@ -1227,6 +1166,7 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstspeex.dll"
Delete "$INSTDIR\gstreamer-plugins\gsttaglib.dll"
Delete "$INSTDIR\gstreamer-plugins\gsttcp.dll"
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
Delete "$INSTDIR\gstreamer-plugins\gsttypefindfunctions.dll"
Delete "$INSTDIR\gstreamer-plugins\gstudp.dll"
Delete "$INSTDIR\gstreamer-plugins\gstvolume.dll"
@@ -1238,14 +1178,9 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
!ifndef arch_arm64
Delete "$INSTDIR\gstreamer-plugins\gstfaac.dll"
Delete "$INSTDIR\gstreamer-plugins\gsttwolame.dll"
!endif
!ifdef arch_x64
Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
;Delete "$INSTDIR\gstreamer-plugins\gstspotify.dll"
!endif
!endif ; msvc
Delete "$INSTDIR\Uninstall.exe"

View File

@@ -90,3 +90,4 @@ class AnalyzerBase : public QWidget {
};
#endif // ANALYZERBASE_H

View File

@@ -73,8 +73,7 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
double_click_timer_(new QTimer(this)),
ignore_next_click_(false),
current_analyzer_(nullptr),
engine_(nullptr),
action_visualization_(nullptr) {
engine_(nullptr) {
QHBoxLayout *layout = new QHBoxLayout(this);
setLayout(layout);
@@ -119,17 +118,6 @@ void AnalyzerContainer::mouseReleaseEvent(QMouseEvent *e) {
}
void AnalyzerContainer::mouseDoubleClickEvent(QMouseEvent *e) {
Q_UNUSED(e);
double_click_timer_->stop();
ignore_next_click_ = true;
if (action_visualization_) action_visualization_->trigger();
}
void AnalyzerContainer::ShowPopupMenu() {
context_menu_->popup(last_click_pos_);
}
@@ -261,10 +249,3 @@ void AnalyzerContainer::AddFramerate(const QString &name, const int framerate) {
QObject::connect(action, &QAction::triggered, this, [this, framerate]() { ChangeFramerate(framerate); } );
}
void AnalyzerContainer::SetVisualizationsAction(QAction *visualization) {
action_visualization_ = visualization;
context_menu_->addAction(action_visualization_);
}

View File

@@ -46,7 +46,6 @@ class AnalyzerContainer : public QWidget {
explicit AnalyzerContainer(QWidget *parent);
void SetEngine(SharedPtr<EngineBase> engine);
void SetVisualizationsAction(QAction *visualization);
static const char *kSettingsGroup;
static const char *kSettingsFramerate;
@@ -56,7 +55,6 @@ class AnalyzerContainer : public QWidget {
protected:
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
private Q_SLOTS:
@@ -91,8 +89,6 @@ class AnalyzerContainer : public QWidget {
AnalyzerBase *current_analyzer_;
SharedPtr<EngineBase> engine_;
QAction *action_visualization_;
};
template<typename T>
@@ -106,6 +102,7 @@ void AnalyzerContainer::AddAnalyzerType() {
action->setCheckable(true);
actions_ << action;
QObject::connect(action, &QAction::triggered, [this, id]() { ChangeAnalyzer(id); } );
}
#endif // ANALYZERCONTAINER_H

View File

@@ -66,7 +66,6 @@ BlockAnalyzer::BlockAnalyzer(QWidget *parent)
// mxcl says null pixmaps cause crashes, so let's play it safe
std::fill(fade_bars_.begin(), fade_bars_.end(), QPixmap(1, 1));
}
void BlockAnalyzer::resizeEvent(QResizeEvent *e) {

View File

@@ -41,14 +41,14 @@ class BlockAnalyzer : public AnalyzerBase {
Q_OBJECT
public:
Q_INVOKABLE explicit BlockAnalyzer(QWidget *parent);
Q_INVOKABLE explicit BlockAnalyzer(QWidget*);
static const char *kName;
protected:
void transform(Scope &s) override;
void transform(Scope&) override;
void analyze(QPainter &p, const Scope &s, const bool new_frame) override;
void resizeEvent(QResizeEvent *e) override;
void resizeEvent(QResizeEvent*) override;
virtual void paletteChange(const QPalette &_palette);
void framerateChanged() override;

View File

@@ -40,7 +40,7 @@ class BoomAnalyzer : public AnalyzerBase {
Q_OBJECT
public:
Q_INVOKABLE explicit BoomAnalyzer(QWidget *parent);
Q_INVOKABLE explicit BoomAnalyzer(QWidget*);
static const char *kName;
@@ -70,6 +70,7 @@ class BoomAnalyzer : public AnalyzerBase {
QPixmap barPixmap_;
QPixmap canvas_;
};
#endif // BOOMANALYZER_H

View File

@@ -55,7 +55,7 @@ class FHT {
/**
* Recursive in-place Hartley transform. For internal use only!
*/
void _transform(float *p, int n, int k);
void _transform(float*, int, int);
public:
/**
@@ -68,7 +68,7 @@ class FHT {
~FHT();
int sizeExp() const;
int size() const;
void scale(float *p, float d) const;
void scale(float*, float) const;
/**
* Exponentially Weighted Moving Average (EWMA) filter.
@@ -90,12 +90,12 @@ class FHT {
/**
* Semi-logarithmic audio spectrum.
*/
void semiLogSpectrum(float *p);
void semiLogSpectrum(float*);
/**
* Fourier spectrum.
*/
void spectrum(float *p);
void spectrum(float*);
/**
* Calculates a mathematically correct FFT power spectrum.
@@ -103,7 +103,7 @@ class FHT {
* and factor the 0.5 in the final scaling factor.
* @see FHT::power2()
*/
void power(float *p);
void power(float*);
/**
* Calculates an FFT power spectrum with doubled values as a
@@ -112,14 +112,14 @@ class FHT {
* of @f$2^n@f$ input values. This is the fastest transform.
* @see FHT::power()
*/
void power2(float *p);
void power2(float*);
/**
* Discrete Hartley transform of data sets with 8 values.
*/
static void transform8(float *p);
static void transform8(float*);
void transform(float *p);
void transform(float*);
};
#endif // FHT_H

View File

@@ -623,7 +623,6 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
CollectionTask task(task_manager_, tr("Updating %1 database.").arg(Song::TextForSource(source_)));
ScopedTransaction transaction(&db);
SongList added_songs;

View File

@@ -140,6 +140,7 @@ class CollectionBackend : public CollectionBackendInterface {
Q_OBJECT
public:
Q_INVOKABLE explicit CollectionBackend(QObject *parent = nullptr);
~CollectionBackend();
@@ -330,3 +331,4 @@ class CollectionBackend : public CollectionBackendInterface {
};
#endif // COLLECTIONBACKEND_H

View File

@@ -26,6 +26,7 @@
class CollectionFilterOptions {
public:
explicit CollectionFilterOptions();
// Filter mode:

View File

@@ -34,7 +34,6 @@
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QRegularExpression>
#include <QInputDialog>
#include <QList>
@@ -296,21 +295,19 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
if (version == 1) {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
const QString &name = saved.at(i);
if (name == "version"_L1) continue;
QByteArray bytes = s.value(name).toByteArray();
if (saved.at(i) == "version"_L1) continue;
QByteArray bytes = s.value(saved.at(i)).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g;
ds >> g;
ret->addAction(CreateGroupByAction(QUrl::fromPercentEncoding(name.toUtf8()), parent, g));
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
}
}
else {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
const QString &name = saved.at(i);
if (name == "version"_L1) continue;
s.remove(name);
if (saved.at(i) == "version"_L1) continue;
s.remove(saved.at(i));
}
}
s.endGroup();
@@ -342,7 +339,7 @@ void CollectionFilterWidget::SaveGroupBy() {
if (!model_) return;
const QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
if (name.isEmpty()) return;
qLog(Debug) << "Saving current grouping to" << name;
@@ -358,7 +355,7 @@ void CollectionFilterWidget::SaveGroupBy() {
QDataStream datastream(&buffer, QIODevice::WriteOnly);
datastream << model_->GetGroupBy();
s.setValue("version", u"1"_s);
s.setValue(QUrl::toPercentEncoding(name), buffer);
s.setValue(name, buffer);
s.endGroup();
UpdateGroupByActions();

View File

@@ -135,3 +135,4 @@ class CollectionFilterWidget : public QWidget {
};
#endif // COLLECTIONFILTERWIDGET_H

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -78,8 +78,6 @@ CollectionLibrary::CollectionLibrary(const SharedPtr<Database> database,
model_ = new CollectionModel(backend_, albumcover_loader, this);
full_rescan_revisions_[21] = tr("Support for sort tags artist, album, album artist, title, composer, and performer");
ReloadSettings();
}
@@ -189,26 +187,6 @@ void CollectionLibrary::ReloadSettings() {
}
void CollectionLibrary::CurrentSongChanged(const Song &song) {
current_song_url_ = song.url();
if (!pending_song_saves_.isEmpty()) {
SavePendingPlaycountsAndRatings();
}
}
void CollectionLibrary::Stopped() {
current_song_url_ = QUrl();
if (!pending_song_saves_.isEmpty()) {
SavePendingPlaycountsAndRatings();
}
}
void CollectionLibrary::SyncPlaycountAndRatingToFilesAsync() {
(void)QtConcurrent::run(&CollectionLibrary::SyncPlaycountAndRatingToFiles, this);
@@ -232,85 +210,18 @@ void CollectionLibrary::SyncPlaycountAndRatingToFiles() {
}
void CollectionLibrary::SongsPlaycountChanged(const SongList &songs, const bool save_tags) {
void CollectionLibrary::SongsPlaycountChanged(const SongList &songs, const bool save_tags) const {
if (save_tags || save_playcounts_to_files_) {
SongList songs_to_save_now;
for (const Song &song : songs) {
if (song.url().isLocalFile() && song.url() == current_song_url_ &&
(song.filetype() == Song::FileType::OggFlac || song.filetype() == Song::FileType::OggVorbis || song.filetype() == Song::FileType::OggOpus)) {
qLog(Debug) << "Deferring playcount save for currently playing file" << song.url().toLocalFile();
if (pending_song_saves_.contains(song.url())) {
SharedPtr<PendingSongSave> pending_song_save = pending_song_saves_[song.url()];
pending_song_save->save_playcount = true;
pending_song_save->song.set_playcount(song.playcount());
}
else {
SharedPtr<PendingSongSave> pending_song_save = make_shared<PendingSongSave>();
pending_song_save->save_playcount = true;
pending_song_save->song = song;
pending_song_saves_.insert(song.url(), pending_song_save);
}
}
else {
songs_to_save_now << song;
}
}
if (!songs_to_save_now.isEmpty()) {
tagreader_client_->SaveSongsPlaycountAsync(songs_to_save_now);
}
tagreader_client_->SaveSongsPlaycountAsync(songs);
}
}
void CollectionLibrary::SongsRatingChanged(const SongList &songs, const bool save_tags) {
void CollectionLibrary::SongsRatingChanged(const SongList &songs, const bool save_tags) const {
if (save_tags || save_ratings_to_files_) {
SongList songs_to_save_now;
for (const Song &song : songs) {
if (song.url().isLocalFile() && song.url() == current_song_url_ &&
(song.filetype() == Song::FileType::OggFlac || song.filetype() == Song::FileType::OggVorbis || song.filetype() == Song::FileType::OggOpus)) {
qLog(Debug) << "Deferring rating save for currently playing file" << song.url().toLocalFile();
if (pending_song_saves_.contains(song.url())) {
SharedPtr<PendingSongSave> pending_song_save = pending_song_saves_[song.url()];
pending_song_save->save_rating = true;
pending_song_save->song.set_rating(song.rating());
}
else {
SharedPtr<PendingSongSave> pending_song_save = make_shared<PendingSongSave>();
pending_song_save->save_rating = true;
pending_song_save->song = song;
pending_song_saves_.insert(song.url(), pending_song_save);
}
}
else {
songs_to_save_now << song;
}
}
if (!songs_to_save_now.isEmpty()) {
tagreader_client_->SaveSongsRatingAsync(songs_to_save_now);
}
}
}
void CollectionLibrary::SavePendingPlaycountsAndRatings() {
for (auto it = pending_song_saves_.constBegin(); it != pending_song_saves_.constEnd();) {
const QUrl url = it.key();
SharedPtr<PendingSongSave> pending_song_save = it.value();
if (url == current_song_url_) {
++it;
continue;
}
qLog(Debug) << "Saving deferred playcount/rating for" << url.toLocalFile();
if (pending_song_save->save_playcount) {
tagreader_client_->SaveSongsPlaycountAsync(SongList() << pending_song_save->song);
}
if (pending_song_save->save_rating) {
tagreader_client_->SaveSongsRatingAsync(SongList() << pending_song_save->song);
}
it = pending_song_saves_.erase(it);
tagreader_client_->SaveSongsRatingAsync(songs);
}
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -27,7 +27,6 @@
#include <QObject>
#include <QList>
#include <QHash>
#include <QMap>
#include <QString>
#include "includes/shared_ptr.h"
@@ -72,7 +71,6 @@ class CollectionLibrary : public QObject {
private:
void SyncPlaycountAndRatingToFiles();
void SavePendingPlaycountsAndRatings();
public Q_SLOTS:
void ReloadSettings();
@@ -86,26 +84,16 @@ class CollectionLibrary : public QObject {
void IncrementalScan();
void CurrentSongChanged(const Song &song);
void Stopped();
private Q_SLOTS:
void ExitReceived();
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false);
void SongsRatingChanged(const SongList &songs, const bool save_tags = false);
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false) const;
void SongsRatingChanged(const SongList &songs, const bool save_tags = false) const;
Q_SIGNALS:
void Error(const QString &error);
void ExitFinished();
private:
class PendingSongSave {
public:
Song song;
bool save_playcount = false;
bool save_rating = false;
};
const SharedPtr<TaskManager> task_manager_;
const SharedPtr<TagReaderClient> tagreader_client_;
@@ -123,10 +111,6 @@ class CollectionLibrary : public QObject {
bool save_playcounts_to_files_;
bool save_ratings_to_files_;
QUrl current_song_url_;
QMap<QUrl, SharedPtr<PendingSongSave>> pending_song_saves_;
};
#endif

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* 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
@@ -54,7 +54,6 @@
#include "includes/scoped_ptr.h"
#include "includes/shared_ptr.h"
#include "constants/collectionsettings.h"
#include "core/logging.h"
#include "core/standardpaths.h"
#include "core/database.h"
@@ -72,12 +71,12 @@
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "covermanager/albumcoverloader.h"
#include "constants/collectionsettings.h"
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
const int CollectionModel::kPrettyCoverSize = 32;
namespace {
constexpr char kPixmapDiskCacheDir[] = "pixmapcache";
constexpr char kVariousArtists[] = QT_TR_NOOP("Various artists");
@@ -89,6 +88,7 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
albumcover_loader_(albumcover_loader),
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)),
use_disk_cache_(false),
@@ -130,6 +130,10 @@ CollectionModel::CollectionModel(const SharedPtr<CollectionBackend> backend, con
backend_->UpdateTotalArtistCountAsync();
backend_->UpdateTotalAlbumCountAsync();
timer_reload_->setSingleShot(true);
timer_reload_->setInterval(300ms);
QObject::connect(timer_reload_, &QTimer::timeout, this, &CollectionModel::Reload);
timer_update_->setSingleShot(false);
timer_update_->setInterval(20ms);
QObject::connect(timer_update_, &QTimer::timeout, this, &CollectionModel::ProcessUpdate);
@@ -187,9 +191,13 @@ void CollectionModel::EndReset() {
}
void CollectionModel::ResetInternal() {
void CollectionModel::Reload() {
loading_ = true;
if (timer_reload_->isActive()) {
timer_reload_->stop();
}
updates_.clear();
options_active_ = options_current_;
@@ -203,6 +211,14 @@ void CollectionModel::ResetInternal() {
}
void CollectionModel::ScheduleReset() {
if (!timer_reload_->isActive()) {
timer_reload_->start();
}
}
void CollectionModel::ReloadSettings() {
Settings settings;
@@ -210,9 +226,7 @@ void CollectionModel::ReloadSettings() {
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_skip_articles_for_artists = settings.value(CollectionSettings::kSkipArticlesForArtists, true).toBool();
const bool sort_skip_articles_for_albums = settings.value(CollectionSettings::kSkipArticlesForAlbums, false).toBool();
const bool use_sort_tags = settings.value(CollectionSettings::kUseSortTags, true).toBool();
const bool sort_skips_articles = settings.value(CollectionSettings::kSortSkipsArticles, true).toBool();
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
@@ -227,15 +241,11 @@ void CollectionModel::ReloadSettings() {
if (show_pretty_covers != options_current_.show_pretty_covers ||
show_dividers != options_current_.show_dividers ||
show_various_artists != options_current_.show_various_artists ||
sort_skip_articles_for_artists != options_current_.sort_skip_articles_for_artists ||
sort_skip_articles_for_albums != options_current_.sort_skip_articles_for_albums ||
use_sort_tags != options_current_.use_sort_tags) {
sort_skips_articles != options_current_.sort_skips_articles) {
options_current_.show_pretty_covers = show_pretty_covers;
options_current_.show_dividers = show_dividers;
options_current_.show_various_artists = show_various_artists;
options_current_.sort_skip_articles_for_artists = sort_skip_articles_for_artists;
options_current_.sort_skip_articles_for_albums = sort_skip_articles_for_albums;
options_current_.use_sort_tags = use_sort_tags;
options_current_.sort_skips_articles = sort_skips_articles;
ScheduleReset();
}
@@ -411,16 +421,11 @@ void CollectionModel::RemoveSongs(const SongList &songs) {
void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) {
if (type == CollectionModelUpdate::Type::Reset) {
updates_.enqueue(CollectionModelUpdate(type));
}
else {
for (qint64 i = 0; i < songs.count(); i += 400LL) {
const qint64 number = std::min(songs.count() - i, 400LL);
const SongList songs_to_queue = songs.mid(i, number);
updates_.enqueue(CollectionModelUpdate(type, songs_to_queue));
}
}
if (!timer_update_->isActive()) {
timer_update_->start();
@@ -428,14 +433,6 @@ void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, con
}
void CollectionModel::ScheduleReset() {
if (!updates_.isEmpty() && updates_.first().type == CollectionModelUpdate::Type::Reset) return;
ScheduleUpdate(CollectionModelUpdate::Type::Reset);
}
void CollectionModel::ScheduleAddSongs(const SongList &songs) {
ScheduleUpdate(CollectionModelUpdate::Type::Add, songs);
@@ -468,9 +465,6 @@ void CollectionModel::ProcessUpdate() {
}
switch (update.type) {
case CollectionModelUpdate::Type::Reset:
ResetInternal();
break;
case CollectionModelUpdate::Type::AddReAddOrUpdate:
AddReAddOrUpdateSongsInternal(update.songs);
break;
@@ -545,10 +539,7 @@ void CollectionModel::AddSongsInternal(const SongList &songs) {
// Sanity check to make sure we don't add songs that are outside the user's filter
if (!options_active_.filter_options.Matches(song)) continue;
if (song_nodes_.contains(song.id())) {
qLog(Debug) << song.id() << song.title() << "already exists, skipping";
continue;
}
if (song_nodes_.contains(song.id())) continue;
// Before we can add each song we need to make sure the required container items already exist in the tree.
// These depend on which "group by" settings the user has on the collection.
@@ -708,7 +699,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
QString divider_key;
if (options_active_.show_dividers && container_level == 0) {
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums, options_active_.use_sort_tags));
divider_key = DividerKey(group_by, song, SortText(group_by, 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);
@@ -722,7 +713,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
item->container_level = container_level;
item->container_key = container_key;
item->display_text = DisplayText(group_by, song);
item->sort_text = SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums, options_active_.use_sort_tags);
item->sort_text = SortText(group_by, song, options_active_.sort_skips_articles);
if (!divider_key.isEmpty()) {
item->sort_text.prepend(divider_key + QLatin1Char(' '));
}
@@ -1077,39 +1068,39 @@ QString CollectionModel::PrettyFormat(const Song &song) {
}
QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums, const bool use_sort_tags) {
QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles) {
switch (group_by) {
case GroupBy::AlbumArtist:
return SortTextForName(use_sort_tags ? song.effective_albumartistsort() : song.effective_albumartist(), sort_skip_articles_for_artists);
return SortTextForArtist(song.effective_albumartist(), sort_skips_articles);
case GroupBy::Artist:
return SortTextForName(use_sort_tags ? song.effective_artistsort() : song.artist(), sort_skip_articles_for_artists);
return SortTextForArtist(song.artist(), sort_skips_articles);
case GroupBy::Album:
return SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums);
return SortText(song.album());
case GroupBy::AlbumDisc:
return SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
return song.album() + SortTextForNumber(std::max(0, song.disc()));
case GroupBy::YearAlbum:
return SortTextForYear(song.year()) + song.grouping() + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums);
return SortTextForNumber(std::max(0, song.year())) + song.grouping() + song.album();
case GroupBy::YearAlbumDisc:
return SortTextForYear(song.year()) + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
return SortTextForNumber(std::max(0, song.year())) + song.album() + SortTextForNumber(std::max(0, song.disc()));
case GroupBy::OriginalYearAlbum:
return SortTextForYear(song.effective_originalyear()) + song.grouping() + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums);
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.grouping() + song.album();
case GroupBy::OriginalYearAlbumDisc:
return SortTextForYear(song.effective_originalyear()) + SortTextForName(use_sort_tags ? song.effective_albumsort() : song.album(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.album() + SortTextForNumber(std::max(0, song.disc()));
case GroupBy::Disc:
return SortTextForNumber(std::max(0, song.disc()));
case GroupBy::Year:
return SortTextForYear(song.year()) + QLatin1Char(' ');
return SortTextForNumber(std::max(0, song.year())) + QLatin1Char(' ');
case GroupBy::OriginalYear:
return SortTextForYear(song.effective_originalyear()) + QLatin1Char(' ');
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
case GroupBy::Genre:
return SortText(song.genre());
return SortTextForArtist(song.genre(), sort_skips_articles);
case GroupBy::Composer:
return SortTextForName(use_sort_tags ? song.effective_composersort() : song.composer(), sort_skip_articles_for_artists);
return SortTextForArtist(song.composer(), sort_skips_articles);
case GroupBy::Performer:
return SortTextForName(use_sort_tags ? song.effective_performersort() : song.performer(), sort_skip_articles_for_artists);
return SortTextForArtist(song.performer(), sort_skips_articles);
case GroupBy::Grouping:
return SortText(song.grouping());
return SortTextForArtist(song.grouping(), sort_skips_articles);
case GroupBy::FileType:
return song.TextForFiletype();
case GroupBy::Format:
@@ -1144,9 +1135,21 @@ QString CollectionModel::SortText(QString text) {
}
QString CollectionModel::SortTextForName(const QString &name, const bool sort_skip_articles) {
QString CollectionModel::SortTextForArtist(QString artist, const bool skip_articles) {
return sort_skip_articles ? SkipArticles(SortText(name)) : SortText(name);
artist = SortText(artist);
if (skip_articles) {
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);
break;
}
}
}
return artist;
}
@@ -1165,32 +1168,18 @@ QString CollectionModel::SortTextForSong(const Song &song) {
QString CollectionModel::SortTextForYear(const int year) {
const QString str = QString::number(std::max(year, 0));
QString str = QString::number(year);
return QStringLiteral("0").repeated(qMax(0, 4 - str.length())) + str;
}
QString CollectionModel::SortTextForBitrate(const int bitrate) {
const QString str = QString::number(bitrate);
QString str = QString::number(bitrate);
return QStringLiteral("0").repeated(qMax(0, 3 - str.length())) + str;
}
QString CollectionModel::SkipArticles(QString name) {
for (const auto &i : Song::kArticles) {
if (name.startsWith(i)) {
qint64 ilen = i.length();
name = name.right(name.length() - ilen) + ", "_L1 + i.left(ilen - 1);
break;
}
}
return name;
}
bool CollectionModel::IsSongTitleDataChanged(const Song &song1, const Song &song2) {
return song1.url() != song2.url() ||
@@ -1221,30 +1210,27 @@ QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song,
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
break;
case GroupBy::AlbumDisc:
key = TextOrUnknown(song.album());
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
key = PrettyAlbumDisc(song.album(), song.disc());
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
break;
case GroupBy::YearAlbum:
key = SortTextForYear(song.year()) + QLatin1Char('-') + TextOrUnknown(song.album());
key = PrettyYearAlbum(song.year(), song.album());
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
break;
case GroupBy::YearAlbumDisc:
key = SortTextForYear(song.year()) + QLatin1Char('-') + TextOrUnknown(song.album());
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
key = PrettyYearAlbumDisc(song.year(), song.album(), song.disc());
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
break;
case GroupBy::OriginalYearAlbum:
key = SortTextForYear(song.effective_originalyear()) + QLatin1Char('-') + TextOrUnknown(song.album());
key = PrettyYearAlbum(song.effective_originalyear(), song.album());
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
break;
case GroupBy::OriginalYearAlbumDisc:
key = SortTextForYear(song.effective_originalyear()) + QLatin1Char('-') + TextOrUnknown(song.album());
key.append(QLatin1Char('-') + SortTextForNumber(song.disc()));
key = PrettyYearAlbumDisc(song.effective_originalyear(), song.album(), song.disc());
if (!song.album_id().isEmpty()) key.append(QLatin1Char('-') + song.album_id());
if (options_active_.separate_albums_by_grouping && !song.grouping().isEmpty()) key.append(QLatin1Char('-') + song.grouping());
break;
@@ -1252,10 +1238,10 @@ QString CollectionModel::ContainerKey(const GroupBy group_by, const Song &song,
key = PrettyDisc(song.disc());
break;
case GroupBy::Year:
key = SortTextForYear(song.year());
key = QString::number(std::max(0, song.year()));
break;
case GroupBy::OriginalYear:
key = SortTextForYear(song.effective_originalyear());
key = QString::number(std::max(0, song.effective_originalyear()));
break;
case GroupBy::Genre:
key = TextOrUnknown(song.genre());
@@ -1347,7 +1333,7 @@ QString CollectionModel::DividerKey(const GroupBy group_by, const Song &song, co
case GroupBy::Bitdepth:
return SortTextForNumber(song.bitdepth());
case GroupBy::Bitrate:
return SortTextForBitrate(song.bitrate());
return SortTextForNumber(song.bitrate());
case GroupBy::None:
case GroupBy::GroupByCount:
return QString();

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* 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
@@ -129,18 +129,14 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
show_dividers(true),
show_pretty_covers(true),
show_various_artists(true),
sort_skip_articles_for_artists(false),
sort_skip_articles_for_albums(false),
use_sort_tags(true),
sort_skips_articles(true),
separate_albums_by_grouping(false) {}
Grouping group_by;
bool show_dividers;
bool show_pretty_covers;
bool show_various_artists;
bool sort_skip_articles_for_artists;
bool sort_skip_articles_for_albums;
bool use_sort_tags;
bool sort_skips_articles;
bool separate_albums_by_grouping;
CollectionFilterOptions filter_options;
};
@@ -187,14 +183,13 @@ 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);
static QString SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums, const bool use_sort_tags);
QString SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles);
static QString SortText(QString text);
static QString SortTextForName(const QString &name, const bool sort_skip_articles);
static QString SortTextForNumber(const int number);
static QString SortTextForArtist(QString artist, const bool skip_articles);
static QString SortTextForSong(const Song &song);
static QString SortTextForYear(const int year);
static QString SortTextForBitrate(const int bitrate);
static QString SkipArticles(QString name);
static bool IsSongTitleDataChanged(const Song &song1, const Song &song2);
QString ContainerKey(const GroupBy group_by, const Song &song, bool &has_unique_album_identifier) const;
@@ -233,7 +228,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
QVariant data(CollectionItem *item, const int role) const;
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs = SongList());
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
void ScheduleAddSongs(const SongList &songs);
void ScheduleUpdateSongs(const SongList &songs);
void ScheduleRemoveSongs(const SongList &songs);
@@ -264,7 +259,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
private Q_SLOTS:
void ResetInternal();
void Reload();
void ScheduleReset();
void ProcessUpdate();
void LoadSongsFromSqlAsyncFinished();
@@ -283,6 +278,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
const SharedPtr<AlbumCoverLoader> albumcover_loader_;
CollectionDirectoryModel *dir_model_;
CollectionFilter *filter_;
QTimer *timer_reload_;
QTimer *timer_update_;
QPixmap pixmap_no_cover_;

View File

@@ -25,13 +25,12 @@
class CollectionModelUpdate {
public:
enum class Type {
Reset,
AddReAddOrUpdate,
Add,
Update,
Remove,
};
explicit CollectionModelUpdate(const Type _type, const SongList &_songs = SongList());
explicit CollectionModelUpdate(const Type _type, const SongList &_songs);
Type type;
SongList songs;
};

View File

@@ -34,6 +34,8 @@ CollectionPlaylistItem::CollectionPlaylistItem(const Song::Source source) : Play
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
int col = 0;
@@ -60,7 +62,7 @@ void CollectionPlaylistItem::Reload() {
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
return;
}
UpdateStreamMetadata(song_);
UpdateTemporaryMetadata(song_);
}
}
@@ -76,9 +78,16 @@ QVariant CollectionPlaylistItem::DatabaseValue(const DatabaseColumn database_col
}
Song CollectionPlaylistItem::Metadata() const {
if (HasTemporaryMetadata()) return temp_metadata_;
return song_;
}
void CollectionPlaylistItem::SetArtManual(const QUrl &cover_url) {
song_.set_art_manual(cover_url);
if (HasStreamMetadata()) stream_song_.set_art_manual(cover_url);
if (HasTemporaryMetadata()) temp_metadata_.set_art_manual(cover_url);
}

View File

@@ -35,17 +35,19 @@ class CollectionPlaylistItem : public PlaylistItem {
explicit CollectionPlaylistItem(const Song::Source source);
explicit CollectionPlaylistItem(const Song &song);
Song OriginalMetadata() const override { return song_; }
void SetOriginalMetadata(const Song &song) override { song_ = song; }
QUrl OriginalUrl() const override { return song_.url(); }
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
QUrl Url() const override;
bool InitFromQuery(const SqlRow &query) override;
void Reload() override;
Song Metadata() const override;
Song OriginalMetadata() const override { return song_; }
void SetMetadata(const Song &song) override { song_ = song; }
void SetArtManual(const QUrl &cover_url) override;
bool IsLocalCollectionItem() const override { return song_.source() == Song::Source::Collection; }
protected:
QVariant DatabaseValue(const DatabaseColumn database_column) const override;
Song DatabaseSongMetadata() const override { return Song(source_); }
@@ -58,3 +60,4 @@ class CollectionPlaylistItem : public PlaylistItem {
};
#endif // COLLECTIONPLAYLISTITEM_H

View File

@@ -65,8 +65,10 @@
#include "collectionitem.h"
#include "collectionitemdelegate.h"
#include "collectionview.h"
#ifndef Q_OS_WIN32
# include "device/devicemanager.h"
# include "device/devicestatefiltermodel.h"
#endif
#include "dialogs/edittagdialog.h"
#include "dialogs/deleteconfirmationdialog.h"
#include "organize/organizedialog.h"
@@ -93,7 +95,9 @@ CollectionView::CollectionView(QWidget *parent)
action_open_in_new_playlist_(nullptr),
action_organize_(nullptr),
action_search_for_this_(nullptr),
#ifndef Q_OS_WIN32
action_copy_to_device_(nullptr),
#endif
action_edit_track_(nullptr),
action_edit_tracks_(nullptr),
action_rescan_songs_(nullptr),
@@ -413,7 +417,9 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
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);
#endif
action_delete_files_ = context_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &CollectionView::Delete);
context_menu_->addSeparator();
@@ -433,8 +439,10 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
context_menu_->addMenu(filter_widget_->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);
#endif
}
@@ -473,7 +481,9 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
action_rescan_songs_->setEnabled(regular_editable > 0);
action_organize_->setVisible(regular_elements == regular_editable);
#ifndef Q_OS_WIN32
action_copy_to_device_->setVisible(regular_elements == regular_editable);
#endif
action_delete_files_->setVisible(delete_files_);
@@ -482,7 +492,9 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
// only when all selected items are editable
action_organize_->setEnabled(regular_elements == regular_editable);
#ifndef Q_OS_WIN32
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
#endif
action_delete_files_->setEnabled(delete_files_);
@@ -747,6 +759,7 @@ void CollectionView::RescanSongs() {
void CollectionView::CopyToDevice() {
#ifndef Q_OS_WIN32
if (!organize_dialog_) {
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
}
@@ -755,6 +768,7 @@ void CollectionView::CopyToDevice() {
organize_dialog_->SetCopy(true);
organize_dialog_->SetSongs(GetSelectedSongs());
organize_dialog_->show();
#endif
}

View File

@@ -138,6 +138,7 @@ class CollectionView : public AutoExpandingTreeView {
void DeleteFilesFinished(const SongList &songs_with_errors);
private:
void RecheckIsEmpty();
void SetShowInVarious(const bool on);
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
void SaveContainerPath(const QModelIndex &child);
@@ -175,7 +176,9 @@ class CollectionView : public AutoExpandingTreeView {
QAction *action_organize_;
QAction *action_search_for_this_;
#ifndef Q_OS_WIN32
QAction *action_copy_to_device_;
#endif
QAction *action_edit_track_;
QAction *action_edit_tracks_;
QAction *action_rescan_songs_;

View File

@@ -706,14 +706,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
qLog(Debug) << file << "is missing EBU R 128 loudness characteristics.";
}
// If the song is unavailable and nothing has changed, just mark it as available without re-scanning
// For CUE files with multiple sections, all sections share the same file and would have the same availability status
if (matching_song.unavailable() && !changed && !missing_fingerprint && !missing_loudness_characteristics) {
qLog(Debug) << "Unavailable song" << file << "restored without re-scanning.";
t->readded_songs << matching_songs;
}
// The song's changed or missing fingerprint - create fingerprint and reread the metadata from file.
else if (t->ignores_mtime() || changed || missing_fingerprint || missing_loudness_characteristics) {
if (t->ignores_mtime() || changed || missing_fingerprint || missing_loudness_characteristics) {
QString fingerprint;
#ifdef HAVE_SONGFINGERPRINTING
@@ -734,6 +728,12 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
}
}
// Nothing has changed - mark the song available without re-scanning
else if (matching_song.unavailable()) {
qLog(Debug) << "Unavailable song" << file << "restored.";
t->readded_songs << matching_songs;
}
}
else { // Search the DB by fingerprint.
QString fingerprint;
@@ -999,18 +999,6 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
changes << u"file path"_s;
notify_new = true;
}
if (matching_song.filetype() != new_song.filetype()) {
changes << u"filetype"_s;
notify_new = true;
}
if (matching_song.filesize() != new_song.filesize()) {
changes << u"filesize"_s;
notify_new = true;
}
if (matching_song.length_nanosec() != new_song.length_nanosec()) {
changes << u"length"_s;
notify_new = true;
}
if (matching_song.fingerprint() != new_song.fingerprint()) {
changes << u"fingerprint"_s;
notify_new = true;
@@ -1046,9 +1034,6 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
if (matching_song.mtime() != new_song.mtime()) {
changes << u"mtime"_s;
}
if (matching_song.ctime() != new_song.ctime()) {
changes << u"ctime"_s;
}
if (changes.isEmpty()) {
qLog(Debug) << "Song" << file << "unchanged.";

View File

@@ -137,7 +137,7 @@ class CollectionWatcher : public QObject {
QStringList files_changed_path_;
private:
ScanTransaction &operator=(const ScanTransaction &transaction) { Q_UNUSED(transaction); return *this; }
ScanTransaction &operator=(const ScanTransaction&) { return *this; }
int task_id_;
quint64 progress_;
@@ -261,6 +261,7 @@ class CollectionWatcher : public QObject {
static QStringList sValidImages;
qint64 last_scan_time_;
};
inline QString CollectionWatcher::NoExtensionPart(const QString &fileName) {

View File

@@ -30,7 +30,6 @@
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QIODevice>
#include <QDataStream>
#include <QKeySequence>
@@ -168,20 +167,14 @@ void SavedGroupingManager::UpdateModel() {
if (version == 1) {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
const QString &name = saved.at(i);
if (name == "version"_L1) continue;
QByteArray bytes = s.value(name).toByteArray();
if (saved.at(i) == "version"_L1) continue;
QByteArray bytes = s.value(saved.at(i)).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g;
ds >> g;
QList<QStandardItem*> list;
QStandardItem *item = new QStandardItem();
item->setText(QUrl::fromPercentEncoding(name.toUtf8()));
item->setData(name);
list << item
list << new QStandardItem(saved.at(i))
<< new QStandardItem(GroupByToString(g.first))
<< new QStandardItem(GroupByToString(g.second))
<< new QStandardItem(GroupByToString(g.third));
@@ -192,9 +185,8 @@ void SavedGroupingManager::UpdateModel() {
else {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
const QString &name = saved.at(i);
if (name == "version"_L1) continue;
s.remove(name);
if (saved.at(i) == "version"_L1) continue;
s.remove(saved.at(i));
}
}
s.endGroup();
@@ -210,7 +202,7 @@ void SavedGroupingManager::Remove() {
for (const QModelIndex &idx : indexes) {
if (idx.isValid()) {
qLog(Debug) << "Remove saved grouping: " << model_->item(idx.row(), 0)->text();
s.remove(model_->item(idx.row(), 0)->data().toString());
s.remove(model_->item(idx.row(), 0)->text());
}
}
s.endGroup();

View File

@@ -48,7 +48,4 @@
#cmakedefine ENABLE_WIN32_CONSOLE
#cmakedefine HAVE_VISUALIZATIONS
#cmakedefine HAVE_PROJECTM4
#endif // CONFIG_H_IN

View File

@@ -70,6 +70,6 @@ enum class BackgroundImagePosition {
BottomRight = 5
};
} // namespace AppearanceSettings
} // namespace
#endif // APPEARANCESETTINGS_H

View File

@@ -63,6 +63,6 @@ constexpr qint64 kDefaultBufferDuration = 4000;
constexpr double kDefaultBufferLowWatermark = 0.33;
constexpr double kDefaultBufferHighWatermark = 0.99;
} // namespace BackendSettings
} // namespace
#endif // BACKENDSETTINGS_H

View File

@@ -71,6 +71,6 @@ constexpr char kDoubleClickPlaylistAddMode[] = "doubleclick_playlist_addmode";
constexpr char kSeekStepSec[] = "seek_step_sec";
constexpr char kVolumeIncrement[] = "volume_increment";
} // namespace BehaviourSettings
} // namespace
#endif // BEHAVIOURSETTINGS_H

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,20 +24,18 @@ namespace CollectionSettings {
constexpr char kSettingsGroup[] = "Collection";
constexpr char kStartupScan[] = "startup_scan";
constexpr char kMonitor[] = "monitor";
constexpr char kSongTracking[] = "song_tracking";
constexpr char kMarkSongsUnavailable[] = "mark_songs_unavailable";
constexpr char kSongENUR128LoudnessAnalysis[] = "song_ebur128_loudness_analysis";
constexpr char kExpireUnavailableSongs[] = "expire_unavailable_songs";
constexpr char kCoverArtPatterns[] = "cover_art_patterns";
constexpr char kAutoOpen[] = "auto_open";
constexpr char kShowDividers[] = "show_dividers";
constexpr char kPrettyCovers[] = "pretty_covers";
constexpr char kVariousArtists[] = "various_artists";
constexpr char kSkipArticlesForArtists[] = "skip_articles_for_artists";
constexpr char kSkipArticlesForAlbums[] = "skip_articles_for_albums";
constexpr char kUseSortTags[] = "use_short_tags";
constexpr char kSortSkipsArticles[] = "sort_skips_articles";
constexpr char kStartupScan[] = "startup_scan";
constexpr char kMonitor[] = "monitor";
constexpr char kSongTracking[] = "song_tracking";
constexpr char kSongENUR128LoudnessAnalysis[] = "song_ebur128_loudness_analysis";
constexpr char kMarkSongsUnavailable[] = "mark_songs_unavailable";
constexpr char kExpireUnavailableSongs[] = "expire_unavailable_songs";
constexpr char kCoverArtPatterns[] = "cover_art_patterns";
constexpr char kSettingsCacheSize[] = "cache_size";
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";
@@ -59,6 +57,6 @@ enum class CacheSizeUnit {
TB
};
} // namespace CollectionSettings
} // namespace
#endif // COLLECTIONSETTINGS_H

View File

@@ -43,6 +43,6 @@ constexpr char kSettingsSummaryFmt[] = "SummaryFmt";
constexpr char kDefaultFontFamily[] = "Noto Sans";
constexpr qreal kDefaultFontSizeHeadline = 11;
} // namespace ContextSettings
} // namespace
#endif // CONTEXTSETTINGS_H

View File

@@ -32,6 +32,6 @@ constexpr char kSaveOverwrite[] = "save_overwrite";
constexpr char kSaveLowercase[] = "save_lowercase";
constexpr char kSaveReplaceSpaces[] = "save_replace_spaces";
} // namespace CoversSettings
} // namespace
#endif // COVERSSETTINGS_H

View File

@@ -27,7 +27,7 @@ constexpr char kAllFilesFilterSpec[] = QT_TRANSLATE_NOOP("FileFilter", "All File
constexpr char kFileFilter[] =
"*.wav *.flac *.wv *.ogg *.oga *.opus *.spx *.ape *.mpc "
"*.mp2 *.mp3 *.m4a *.mp4 *.aac *.asf *.asx *.wma "
"*.aif *.aiff *.mka *.tta *.dsf *.dsd *.webm "
"*.aif *.aiff *.mka *.tta *.dsf *.dsd "
"*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini "
"*.ac3 *.dts "
"*.mod *.s3m *.xm *.it"

View File

@@ -26,6 +26,6 @@ constexpr char kSettingsGroup[] = "GlobalShortcuts";
constexpr char kUseKGlobalAccel[] = "use_kglobalaccel";
constexpr char kUseX11[] = "use_x11";
} // namespace GlobalShortcutsSettings
} // namespace
#endif // GLOBALSHORTCUTSSETTINGS_H

View File

@@ -25,6 +25,6 @@ namespace LyricsSettings {
constexpr char kSettingsGroup[] = "Lyrics";
constexpr char kProviders[] = "providers";
} // namespace LyricsSettings
} // namespace
#endif // LYRICSSETTINGS_H

View File

@@ -32,6 +32,6 @@ constexpr char kGeometry[] = "geometry";
constexpr char kSplitterState[] = "splitter_state";
constexpr char kDoNotShowSponsorMessage[] = "do_not_show_sponsor_message";
} // namespace MainWindowSettings
} // namespace
#endif // MAINWINDOWSETTINGS_H

View File

@@ -38,6 +38,6 @@ constexpr char kShow[] = "show";
constexpr char kStyle[] = "style";
constexpr char kSave[] = "save";
} // namespace MoodbarSettings
} // namespace
#endif // MOODBARSETTINGS_H

View File

@@ -32,6 +32,6 @@ constexpr char kUsername[] = "username";
constexpr char kPassword[] = "password";
constexpr char kEngine[] = "engine";
} // namespace NetworkProxySettings
} // namespace
#endif // NETWORKPROXYSETTINGS_H

View File

@@ -45,7 +45,7 @@ constexpr char kCustomTextEnabled[] = "CustomTextEnabled";
constexpr char kCustomText1[] = "CustomText1";
constexpr char kCustomText2[] = "CustomText2";
} // namespace OSDSettings
} // namespace
namespace OSDPrettySettings {
@@ -63,7 +63,7 @@ constexpr char kFading[] = "fading";
constexpr QRgb kPresetBlue = qRgb(102, 150, 227);
constexpr QRgb kPresetRed = qRgb(202, 22, 16);
} // namespace OSDPrettySettings
} // namespace
namespace DiscordRPCSettings {
@@ -71,14 +71,6 @@ constexpr char kSettingsGroup[] = "DiscordRPC";
constexpr char kEnabled[] = "enabled";
constexpr char kStatusDisplayType[] = "StatusDisplayType";
enum class StatusDisplayType {
App = 0,
Artist,
Song
};
} // namespace DiscordRPCSettings
} // namespace
#endif // NOTIFICATIONSSETTINGS_H

View File

@@ -63,7 +63,7 @@ constexpr char kLastSaveExtension[] = "last_save_extension";
constexpr char kLastSaveAllPath[] = "last_save_all_path";
constexpr char kLastSaveAllExtension[] = "last_save_all_extension";
} // namespace PlaylistSettings
} // namespace
Q_DECLARE_METATYPE(PlaylistSettings::PathType)

View File

@@ -36,13 +36,12 @@ constexpr char kAlbumsSearchLimit[] = "albumssearchlimit";
constexpr char kSongsSearchLimit[] = "songssearchlimit";
constexpr char kBase64Secret[] = "base64secret";
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
constexpr char kRemoveRemastered[] = "remove_remastered";
constexpr char kUserId[] = "user_id";
constexpr char kCredentialsId[] = "credentials_id";
constexpr char kDeviceId[] = "device_id";
constexpr char kUserAuthToken[] = "user_auth_token";
} // namespace QobuzSettings
} // namespace
#endif // QOBUZSETTINGS_H

View File

@@ -35,6 +35,6 @@ constexpr char kStripRemastered[] = "strip_remastered";
constexpr char kSources[] = "sources";
constexpr char kUserToken[] = "user_token";
} // namespace ScrobblerSettings
} // namespace
#endif // SCROBBLERSETTINGS_H

View File

@@ -31,13 +31,12 @@ constexpr char kAlbumsSearchLimit[] = "albumssearchlimit";
constexpr char kSongsSearchLimit[] = "songssearchlimit";
constexpr char kFetchAlbums[] = "fetchalbums";
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
constexpr char kRemoveRemastered[] = "remove_remastered";
constexpr char kAccessToken[] = "access_token";
constexpr char kRefreshToken[] = "refresh_token";
constexpr char kExpiresIn[] = "expires_in";
constexpr char kLoginTime[] = "login_time";
} // namespace SpotifySettings
} // namespace
#endif // SPOTIFYSETTINGS_H

View File

@@ -41,6 +41,6 @@ constexpr char kUseAlbumIdForAlbumCovers[] = "usealbumidforalbumcovers";
constexpr char kServerSideScrobbling[] = "serversidescrobbling";
constexpr char kAuthMethod[] = "authmethod";
} // namespace SubsonicSettings
} // namespace
#endif // SUBSONICETTINGS_H

View File

@@ -40,7 +40,6 @@ constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
constexpr char kCoverSize[] = "coversize";
constexpr char kStreamUrl[] = "streamurl";
constexpr char kAlbumExplicit[] = "album_explicit";
constexpr char kRemoveRemastered[] = "remove_remastered";
enum class StreamUrlMethod {
StreamUrl,
@@ -48,6 +47,6 @@ enum class StreamUrlMethod {
PlaybackInfoPostPaywall
};
} // namespace TidalSettings
}
#endif // TIDALSETTINGS_H

View File

@@ -26,3 +26,4 @@ constexpr char kSettingsGroup[] = "Transcoder";
}
#endif // TRANSCODERSETTINGS_H

View File

@@ -61,6 +61,7 @@ class ContextAlbum : public QWidget {
void contextMenuEvent(QContextMenuEvent *e) override;
private:
struct PreviousCover {
explicit PreviousCover() : opacity(0.0) {}
QImage image;

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2013-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2013-2022, 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
@@ -59,7 +59,6 @@
#include "covermanager/albumcoverchoicecontroller.h"
#include "lyrics/lyricsfetcher.h"
#include "constants/contextsettings.h"
#include "constants/timeconstants.h"
#include "contextview.h"
#include "contextalbum.h"
@@ -101,11 +100,15 @@ ContextView::ContextView(QWidget *parent)
label_samplerate_title_(new QLabel(this)),
label_bitdepth_title_(new QLabel(this)),
label_bitrate_title_(new QLabel(this)),
label_ebur128_integrated_loudness_title_(new QLabel(this)),
label_ebur128_loudness_range_title_(new QLabel(this)),
label_filetype_(new QLabel(this)),
label_length_(new QLabel(this)),
label_samplerate_(new QLabel(this)),
label_bitdepth_(new QLabel(this)),
label_bitrate_(new QLabel(this)),
label_ebur128_integrated_loudness_(new QLabel(this)),
label_ebur128_loudness_range_(new QLabel(this)),
lyrics_tried_(false),
lyrics_id_(-1) {
@@ -163,18 +166,24 @@ ContextView::ContextView(QWidget *parent)
label_samplerate_title_->setText(tr("Samplerate"));
label_bitdepth_title_->setText(tr("Bit depth"));
label_bitrate_title_->setText(tr("Bitrate"));
label_ebur128_integrated_loudness_title_->setText(tr("EBU R 128 Integrated Loudness"));
label_ebur128_loudness_range_title_->setText(tr("EBU R 128 Loudness Range"));
label_filetype_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
label_length_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
label_samplerate_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
label_bitdepth_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
label_bitrate_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
label_ebur128_integrated_loudness_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
label_ebur128_loudness_range_title_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
label_filetype_->setWordWrap(true);
label_length_->setWordWrap(true);
label_samplerate_->setWordWrap(true);
label_bitdepth_->setWordWrap(true);
label_bitrate_->setWordWrap(true);
label_ebur128_integrated_loudness_->setWordWrap(true);
label_ebur128_loudness_range_->setWordWrap(true);
layout_play_data_->setContentsMargins(0, 0, 0, 0);
layout_play_data_->addWidget(label_filetype_title_, 0, 0);
@@ -188,6 +197,11 @@ ContextView::ContextView(QWidget *parent)
layout_play_data_->addWidget(label_bitrate_title_, 4, 0);
layout_play_data_->addWidget(label_bitrate_, 4, 1);
layout_play_data_->addWidget(label_ebur128_integrated_loudness_title_, 5, 0);
layout_play_data_->addWidget(label_ebur128_integrated_loudness_, 5, 1);
layout_play_data_->addWidget(label_ebur128_loudness_range_title_, 6, 0);
layout_play_data_->addWidget(label_ebur128_loudness_range_, 6, 1);
widget_play_data_->setLayout(layout_play_data_);
textedit_play_lyrics_->setReadOnly(true);
@@ -204,13 +218,17 @@ ContextView::ContextView(QWidget *parent)
<< label_length_title_
<< label_samplerate_title_
<< label_bitdepth_title_
<< label_bitrate_title_;
<< label_bitrate_title_
<< label_ebur128_integrated_loudness_title_
<< label_ebur128_loudness_range_title_;
labels_play_data_ << label_filetype_
<< label_length_
<< label_samplerate_
<< label_bitdepth_
<< label_bitrate_;
<< label_bitrate_
<< label_ebur128_integrated_loudness_
<< label_ebur128_loudness_range_;
labels_play_all_ = labels_play_ << labels_play_data_;
@@ -354,7 +372,7 @@ void ContextView::SearchLyrics() {
if (lyrics_.isEmpty() && action_show_lyrics_->isChecked() && action_search_lyrics_->isChecked() && !song_playing_.artist().isEmpty() && !song_playing_.title().isEmpty() && !lyrics_tried_ && lyrics_id_ == -1) {
lyrics_fetcher_->Clear();
lyrics_tried_ = true;
lyrics_id_ = static_cast<qint64>(lyrics_fetcher_->Search(song_playing_.effective_albumartist(), song_playing_.artist(), song_playing_.album(), song_playing_.title(), song_playing_.length_nanosec() / kNsecPerSec));
lyrics_id_ = static_cast<qint64>(lyrics_fetcher_->Search(song_playing_.effective_albumartist(), song_playing_.artist(), song_playing_.album(), song_playing_.title()));
}
}
@@ -378,7 +396,7 @@ void ContextView::UpdateNoSong() {
void ContextView::NoSong() {
if (!widget_album_->isVisibleTo(this)) {
if (!widget_album_->isVisible()) {
widget_album_->show();
}
@@ -422,11 +440,11 @@ void ContextView::SetSong() {
label_stop_summary_->clear();
bool widget_album_changed = !song_prev_.is_valid();
if (action_show_album_->isChecked() && !widget_album_->isVisibleTo(this)) {
if (action_show_album_->isChecked() && !widget_album_->isVisible()) {
widget_album_->show();
widget_album_changed = true;
}
else if (!action_show_album_->isChecked() && widget_album_->isVisibleTo(this)) {
else if (!action_show_album_->isChecked() && widget_album_->isVisible()) {
widget_album_->hide();
widget_album_changed = true;
}
@@ -475,6 +493,26 @@ void ContextView::SetSong() {
label_bitrate_->show();
SetLabelText(label_bitrate_, song_playing_.bitrate(), tr("kbps"));
}
if (!song_playing_.ebur128_integrated_loudness_lufs()) {
label_ebur128_integrated_loudness_title_->hide();
label_ebur128_integrated_loudness_->hide();
label_ebur128_integrated_loudness_->clear();
}
else {
label_ebur128_integrated_loudness_title_->show();
label_ebur128_integrated_loudness_->show();
label_ebur128_integrated_loudness_->setText(song_playing_.Ebur128LoudnessLUFSToText());
}
if (!song_playing_.ebur128_loudness_range_lu()) {
label_ebur128_loudness_range_title_->hide();
label_ebur128_loudness_range_->hide();
label_ebur128_loudness_range_->clear();
}
else {
label_ebur128_loudness_range_title_->show();
label_ebur128_loudness_range_->show();
label_ebur128_loudness_range_->setText(song_playing_.Ebur128LoudnessRangeLUToText());
}
spacer_play_data_->changeSize(20, 20, QSizePolicy::Fixed);
}
else {
@@ -484,6 +522,8 @@ void ContextView::SetSong() {
label_samplerate_->clear();
label_bitdepth_->clear();
label_bitrate_->clear();
label_ebur128_integrated_loudness_->clear();
label_ebur128_loudness_range_->clear();
spacer_play_data_->changeSize(0, 0, QSizePolicy::Fixed);
}
@@ -558,6 +598,12 @@ void ContextView::UpdateSong(const Song &song) {
SetLabelText(label_bitrate_, song.bitrate(), tr("kbps"));
}
}
if (song.ebur128_integrated_loudness_lufs() != song_playing_.ebur128_integrated_loudness_lufs()) {
label_ebur128_integrated_loudness_->setText(song_playing_.Ebur128LoudnessLUFSToText());
}
if (song.ebur128_loudness_range_lu() != song_playing_.ebur128_loudness_range_lu()) {
label_ebur128_loudness_range_->setText(song_playing_.Ebur128LoudnessRangeLUToText());
}
}
song_playing_ = song;

View File

@@ -1,6 +1,6 @@
/*
* Strawberry Music Player
* Copyright 2013-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2013-2022, 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
@@ -65,9 +65,9 @@ class ContextView : public QWidget {
protected:
void resizeEvent(QResizeEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void dragEnterEvent(QDragEnterEvent *e) override;
void dropEvent(QDropEvent *e) override;
void contextMenuEvent(QContextMenuEvent*) override;
void dragEnterEvent(QDragEnterEvent*) override;
void dropEvent(QDropEvent*) override;
private:
void AddActions();
@@ -135,12 +135,18 @@ class ContextView : public QWidget {
QLabel *label_bitdepth_title_;
QLabel *label_bitrate_title_;
QLabel *label_ebur128_integrated_loudness_title_;
QLabel *label_ebur128_loudness_range_title_;
QLabel *label_filetype_;
QLabel *label_length_;
QLabel *label_samplerate_;
QLabel *label_bitdepth_;
QLabel *label_bitrate_;
QLabel *label_ebur128_integrated_loudness_;
QLabel *label_ebur128_loudness_range_;
Song song_playing_;
Song song_prev_;
QImage image_original_;

View File

@@ -74,10 +74,10 @@
#include "lyrics/elyricsnetlyricsprovider.h"
#include "lyrics/letraslyricsprovider.h"
#include "lyrics/lyricfindlyricsprovider.h"
#include "lyrics/lrcliblyricsprovider.h"
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/lastfmscrobbler.h"
#include "scrobbler/librefmscrobbler.h"
#include "scrobbler/listenbrainzscrobbler.h"
#include "scrobbler/lastfmimport.h"
#ifdef HAVE_SUBSONIC
@@ -118,8 +118,8 @@ using namespace std::chrono_literals;
class ApplicationImpl {
public:
explicit ApplicationImpl(Application *app)
: tagreader_client_([app]() {
explicit ApplicationImpl(Application *app) :
tagreader_client_([app](){
TagReaderClient *client = new TagReaderClient();
app->MoveToNewThread(client);
return client;
@@ -183,7 +183,6 @@ class ApplicationImpl {
lyrics_providers->AddProvider(new ElyricsNetLyricsProvider(lyrics_providers->network()));
lyrics_providers->AddProvider(new LetrasLyricsProvider(lyrics_providers->network()));
lyrics_providers->AddProvider(new LyricFindLyricsProvider(lyrics_providers->network()));
lyrics_providers->AddProvider(new LrcLibLyricsProvider(lyrics_providers->network()));
lyrics_providers->ReloadSettings();
return lyrics_providers;
}),
@@ -207,6 +206,7 @@ class ApplicationImpl {
scrobbler_([app]() {
AudioScrobbler *scrobbler = new AudioScrobbler(app);
scrobbler->AddService(make_shared<LastFMScrobbler>(scrobbler->settings(), app->network()));
scrobbler->AddService(make_shared<LibreFMScrobbler>(scrobbler->settings(), app->network()));
scrobbler->AddService(make_shared<ListenBrainzScrobbler>(scrobbler->settings(), app->network()));
#ifdef HAVE_SUBSONIC
scrobbler->AddService(make_shared<SubsonicScrobbler>(scrobbler->settings(), app->network(), app->streaming_services()->Service<SubsonicService>(), app));

View File

@@ -50,7 +50,7 @@
using namespace Qt::Literals::StringLiterals;
const int Database::kSchemaVersion = 21;
const int Database::kSchemaVersion = 20;
namespace {
constexpr char kDatabaseFilename[] = "strawberry.db";
@@ -61,8 +61,8 @@ constexpr char kMagicAllSongsTables[] = "%allsongstables";
int Database::sNextConnectionId = 1;
QMutex Database::sNextConnectionIdMutex;
Database::Database(SharedPtr<TaskManager> task_manager, QObject *parent, const QString &database_name)
: QObject(parent),
Database::Database(SharedPtr<TaskManager> task_manager, QObject *parent, const QString &database_name) :
QObject(parent),
task_manager_(task_manager),
injected_database_name_(database_name),
query_hash_(0),
@@ -414,6 +414,11 @@ void Database::ExecSongTablesCommands(QSqlDatabase &db, const QStringList &song_
// We allow a magic value in the schema files to update all songs tables at once.
if (command.contains(QLatin1String(kMagicAllSongsTables))) {
for (const QString &table : song_tables) {
// Another horrible hack: device songs tables don't have matching _fts tables, so if this command tries to touch one, ignore it.
if (table.startsWith("device_"_L1) && command.contains(QLatin1String(kMagicAllSongsTables) + "_fts"_L1)) {
continue;
}
qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables;
QString new_command(command);
new_command.replace(QLatin1String(kMagicAllSongsTables), table);
@@ -503,9 +508,7 @@ bool Database::IntegrityCheck(const QSqlDatabase &db) {
break;
}
else {
if (!error_reported) {
Q_EMIT Error(tr("Database corruption detected."));
}
if (!error_reported) { Q_EMIT Error(tr("Database corruption detected.")); }
Q_EMIT Error(u"Database: "_s + message);
error_reported = true;
}
@@ -596,7 +599,8 @@ void Database::BackupFile(const QString &filename) {
ret = sqlite3_backup_step(backup, 16);
const int page_count = sqlite3_backup_pagecount(backup);
task_manager_->SetTaskProgress(task_id, static_cast<quint64>(page_count - sqlite3_backup_remaining(backup)), static_cast<quint64>(page_count));
} while (ret == SQLITE_OK);
}
while (ret == SQLITE_OK);
if (ret != SQLITE_DONE) {
qLog(Error) << "Database backup failed";

View File

@@ -128,6 +128,7 @@ class Database : public QObject {
int startup_schema_version_;
QThread *original_thread_;
};
#endif // DATABASE_H

View File

@@ -110,32 +110,21 @@ bool FilesystemMusicStorage::CopyToStorage(const CopyJob &job, QString &error_te
bool FilesystemMusicStorage::DeleteFromStorage(const DeleteJob &job) {
const QString path = job.metadata_.url().toLocalFile();
const QFileInfo fileInfo(path);
QString path = job.metadata_.url().toLocalFile();
QFileInfo fileInfo(path);
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
if (job.use_trash_ && QFile::supportsMoveToTrash()) {
#else
if (job.use_trash_) {
#endif
if (QFile::moveToTrash(path)) {
return true;
}
qLog(Warning) << "Moving file to trash failed for" << path << ", falling back to direct deletion";
return QFile::moveToTrash(path);
}
bool success = false;
if (fileInfo.isDir()) {
success = Utilities::RemoveRecursive(path);
}
else {
success = QFile::remove(path);
return Utilities::RemoveRecursive(path);
}
if (!success) {
qLog(Error) << "Failed to delete file" << path;
}
return success;
return QFile::remove(path);
}

View File

@@ -157,16 +157,12 @@ void HttpBaseRequest::HandleSSLErrors(const QList<QSslError> &ssl_errors) {
HttpBaseRequest::ReplyDataResult HttpBaseRequest::GetReplyData(QNetworkReply *reply) {
if (reply->error() != QNetworkReply::NoError) {
if (reply->error() >= 200) {
reply->readAll(); // QTBUG-135641
}
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
const int http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (http_status_code < 200 || http_status_code > 207) {
reply->readAll(); // QTBUG-135641
return ReplyDataResult(ErrorCode::HttpError, QStringLiteral("Received HTTP code %1").arg(http_status_code));
}
}

View File

@@ -27,7 +27,6 @@ class IconLoader {
public:
static void Init();
static QIcon Load(const QString &name, const bool system_icon = true, const int fixed_size = 0, const int min_size = 0, const int max_size = 0);
private:
explicit IconLoader() {}
static bool system_icons_;

View File

@@ -155,7 +155,11 @@ void LocalRedirectServer::WriteTemplate() const {
QBuffer image_buffer;
if (image_buffer.open(QIODevice::ReadWrite)) {
QApplication::style()->standardIcon(QStyle::SP_DialogOkButton).pixmap(16).toImage().save(&image_buffer, "PNG");
QApplication::style()
->standardIcon(QStyle::SP_DialogOkButton)
.pixmap(16)
.toImage()
.save(&image_buffer, "PNG");
page_data.replace("@IMAGE_DATA@"_L1, QString::fromUtf8(image_buffer.data().toBase64()));
image_buffer.close();
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2013-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2013-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
@@ -58,6 +58,7 @@
#include <QShortcut>
#include <QMessageBox>
#include <QErrorMessage>
#include <QSettings>
#include <QColor>
#include <QFrame>
#include <QItemSelectionModel>
@@ -155,8 +156,10 @@
#include "lyrics/lyricsproviders.h"
#include "device/devicemanager.h"
#include "device/devicestatefiltermodel.h"
#ifndef Q_OS_WIN32
# include "device/deviceview.h"
# include "device/deviceviewcontainer.h"
#endif
#include "transcoder/transcodedialog.h"
#include "settings/settingsdialog.h"
#include "constants/behavioursettings.h"
@@ -172,7 +175,6 @@
# include "constants/tidalsettings.h"
#endif
#ifdef HAVE_SPOTIFY
# include "spotify/spotifyservice.h"
# include "constants/spotifysettings.h"
#endif
#ifdef HAVE_QOBUZ
@@ -206,11 +208,6 @@
#include "organize/organizeerrordialog.h"
#ifdef HAVE_VISUALIZATIONS
# include "visualizations/visualizationcontainer.h"
# include "engine/gstengine.h"
#endif
#ifdef Q_OS_WIN32
# include "core/windows7thumbbar.h"
#endif
@@ -283,7 +280,7 @@ constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-
#endif // HAVE_QTSPARKLE
MainWindow::MainWindow(Application *app,
SharedPtr<SystemTrayIcon> systemtrayicon, OSDBase *osd,
SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd,
#ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence,
#endif
@@ -295,7 +292,7 @@ MainWindow::MainWindow(Application *app,
thumbbar_(new Windows7ThumbBar(this)),
#endif
app_(app),
systemtrayicon_(systemtrayicon),
tray_icon_(tray_icon),
osd_(osd),
#ifdef HAVE_DISCORD_RPC
discord_rich_presence_(discord_rich_presence),
@@ -313,7 +310,9 @@ MainWindow::MainWindow(Application *app,
context_view_(new ContextView(this)),
collection_view_(new CollectionViewContainer(this)),
file_view_(new FileView(this)),
#ifndef Q_OS_WIN32
device_view_(new DeviceViewContainer(this)),
#endif
playlist_list_(new PlaylistListContainer(this)),
queue_view_(new QueueView(this)),
settings_dialog_(std::bind(&MainWindow::CreateSettingsDialog, this)),
@@ -376,7 +375,9 @@ MainWindow::MainWindow(Application *app,
playlist_move_to_collection_(nullptr),
playlist_open_in_browser_(nullptr),
playlist_organize_(nullptr),
#ifndef Q_OS_WIN32
playlist_copy_to_device_(nullptr),
#endif
playlist_delete_(nullptr),
playlist_queue_(nullptr),
playlist_queue_play_next_(nullptr),
@@ -408,11 +409,7 @@ MainWindow::MainWindow(Application *app,
// Initialize the UI
ui_->setupUi(this);
if (QGuiApplication::platformName() != "wayland"_L1) {
setWindowIcon(IconLoader::Load(u"strawberry"_s));
}
systemtrayicon_->SetDevicePixelRatioF(devicePixelRatioF());
QObject::connect(&*app->database(), &Database::Error, this, &MainWindow::ShowErrorDialog);
@@ -433,7 +430,9 @@ MainWindow::MainWindow(Application *app,
ui_->tabs->AddTab(smartplaylists_view_, u"smartplaylists"_s, IconLoader::Load(u"view-media-playlist"_s, true, 0, 32), tr("Smart playlists"));
ui_->tabs->AddTab(file_view_, u"files"_s, IconLoader::Load(u"document-open"_s, true, 0, 32), tr("Files"));
ui_->tabs->AddTab(radio_view_, u"radios"_s, IconLoader::Load(u"radio"_s, true, 0, 32), tr("Radios"));
#ifndef Q_OS_WIN32
ui_->tabs->AddTab(device_view_, u"devices"_s, IconLoader::Load(u"device"_s, true, 0, 32), tr("Devices"));
#endif
#ifdef HAVE_SUBSONIC
ui_->tabs->AddTab(subsonic_view_, u"subsonic"_s, IconLoader::Load(u"subsonic"_s, true, 0, 32), tr("Subsonic"));
#endif
@@ -481,7 +480,9 @@ MainWindow::MainWindow(Application *app,
collection_view_->view()->setModel(app_->collection()->model()->filter());
collection_view_->view()->Init(app->task_manager(), app->tagreader_client(), app->network(), app->albumcover_loader(), app->current_albumcover_loader(), app->cover_providers(), app->lyrics_providers(), app->collection(), app->device_manager(), app->streaming_services());
#ifndef Q_OS_WIN32
device_view_->view()->Init(app->task_manager(), app->tagreader_client(), app->device_manager(), app->collection_model()->directory_model());
#endif
playlist_list_->Init(app_->task_manager(), app->tagreader_client(), app_->playlist_manager(), app_->playlist_backend(), app_->device_manager());
organize_dialog_->SetDestinationModel(app_->collection()->model()->directory_model());
@@ -553,7 +554,9 @@ MainWindow::MainWindow(Application *app,
QObject::connect(file_view_, &FileView::CopyToCollection, this, &MainWindow::CopyFilesToCollection);
QObject::connect(file_view_, &FileView::MoveToCollection, this, &MainWindow::MoveFilesToCollection);
QObject::connect(file_view_, &FileView::EditTags, this, &MainWindow::EditFileTags);
#ifndef Q_OS_WIN32
QObject::connect(file_view_, &FileView::CopyToDevice, this, &MainWindow::CopyFilesToDevice);
#endif
file_view_->SetTaskManager(app_->task_manager());
// Action connections
@@ -628,12 +631,6 @@ MainWindow::MainWindow(Application *app,
stop_menu->addAction(ui_->action_stop_after_this_track);
ui_->stop_button->setMenu(stop_menu);
#ifdef HAVE_VISUALIZATIONS
QObject::connect(ui_->action_visualizations, &QAction::triggered, this, &MainWindow::ShowVisualizations);
#else
ui_->action_visualizations->setEnabled(false);
#endif
// Player connections
QObject::connect(ui_->volume, &VolumeSlider::valueChanged, &*app_->player(), &Player::SetVolumeFromSlider);
@@ -707,9 +704,6 @@ MainWindow::MainWindow(Application *app,
QObject::connect(&*app_->task_manager(), &TaskManager::PauseCollectionWatchers, &*app_->collection(), &CollectionLibrary::PauseWatcher);
QObject::connect(&*app_->task_manager(), &TaskManager::ResumeCollectionWatchers, &*app_->collection(), &CollectionLibrary::ResumeWatcher);
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, &*app_->collection(), &CollectionLibrary::CurrentSongChanged);
QObject::connect(&*app_->player(), &Player::Stopped, &*app_->collection(), &CollectionLibrary::Stopped);
QObject::connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, &*app_->current_albumcover_loader(), &CurrentAlbumCoverLoader::LoadAlbumCover);
QObject::connect(&*app_->current_albumcover_loader(), &CurrentAlbumCoverLoader::AlbumCoverLoaded, this, &MainWindow::AlbumCoverLoaded);
QObject::connect(album_cover_choice_controller_, &AlbumCoverChoiceController::Error, this, &MainWindow::ShowErrorDialog);
@@ -724,8 +718,10 @@ MainWindow::MainWindow(Application *app,
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::triggered, this, &MainWindow::SearchCoverAutomatically);
QObject::connect(album_cover_choice_controller_->search_cover_auto_action(), &QAction::toggled, this, &MainWindow::ToggleSearchCoverAuto);
#ifndef Q_OS_WIN32
// Devices connections
QObject::connect(device_view_->view(), &DeviceView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
#endif
// Collection filter widget
QActionGroup *collection_view_group = new QActionGroup(this);
@@ -788,9 +784,6 @@ MainWindow::MainWindow(Application *app,
QObject::connect(spotify_view_->songs_collection_view(), &StreamingCollectionView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
QObject::connect(spotify_view_->search_view(), &StreamingSearchView::OpenSettingsDialog, this, &MainWindow::OpenServiceSettingsDialog);
QObject::connect(spotify_view_->search_view(), &StreamingSearchView::AddToPlaylist, this, &MainWindow::AddToPlaylist);
if (SpotifyServicePtr spotifyservice = app_->streaming_services()->Service<SpotifyService>()) {
QObject::connect(&*spotifyservice, &SpotifyService::UpdateSpotifyAccessToken, &*app_->player()->engine(), &EngineBase::UpdateSpotifyAccessToken);
}
#endif
QObject::connect(radio_view_, &RadioViewContainer::Refresh, &*app_->radio_services(), &RadioServices::RefreshChannels);
@@ -831,7 +824,9 @@ MainWindow::MainWindow(Application *app,
playlist_organize_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Organize files..."), this, &MainWindow::PlaylistMoveToCollection);
playlist_copy_to_collection_ = playlist_menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &MainWindow::PlaylistCopyToCollection);
playlist_move_to_collection_ = playlist_menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &MainWindow::PlaylistMoveToCollection);
#ifndef Q_OS_WIN32
playlist_copy_to_device_ = playlist_menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &MainWindow::PlaylistCopyToDevice);
#endif
playlist_delete_ = playlist_menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &MainWindow::PlaylistDelete);
playlist_menu_->addSeparator();
playlistitem_actions_separator_ = playlist_menu_->addSeparator();
@@ -850,8 +845,10 @@ MainWindow::MainWindow(Application *app,
QObject::connect(ui_->playlist, &PlaylistContainer::UndoRedoActionsChanged, this, &MainWindow::PlaylistUndoRedoChanged);
QObject::connect(&*app_->device_manager(), &DeviceManager::DeviceError, this, &MainWindow::ShowErrorDialog);
#ifndef WIN32
QObject::connect(app_->device_manager()->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, playlist_copy_to_device_, &QAction::setDisabled);
playlist_copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
#endif
QObject::connect(&*app_->scrobbler()->settings(), &ScrobblerSettingsService::ScrobblingEnabledChanged, this, &MainWindow::ScrobblingEnabledChanged);
QObject::connect(&*app_->scrobbler()->settings(), &ScrobblerSettingsService::ScrobbleButtonVisibilityChanged, this, &MainWindow::ScrobbleButtonVisibilityChanged);
@@ -861,14 +858,14 @@ MainWindow::MainWindow(Application *app,
mac::SetApplicationHandler(this);
#endif
// Tray icon
systemtrayicon_->SetupMenu(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_stop_after_this_track, ui_->action_next_track, ui_->action_mute, ui_->action_love, ui_->action_quit);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
QObject::connect(&*systemtrayicon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
tray_icon_->SetupMenu(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_stop_after_this_track, ui_->action_next_track, ui_->action_mute, ui_->action_love, ui_->action_quit);
QObject::connect(&*tray_icon_, &SystemTrayIcon::PlayPause, &*app_->player(), &Player::PlayPauseHelper);
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekForward, &*app_->player(), &Player::SeekForward);
QObject::connect(&*tray_icon_, &SystemTrayIcon::SeekBackward, &*app_->player(), &Player::SeekBackward);
QObject::connect(&*tray_icon_, &SystemTrayIcon::NextTrack, &*app_->player(), &Player::Next);
QObject::connect(&*tray_icon_, &SystemTrayIcon::PreviousTrack, &*app_->player(), &Player::Previous);
QObject::connect(&*tray_icon_, &SystemTrayIcon::ShowHide, this, &MainWindow::ToggleShowHide);
QObject::connect(&*tray_icon_, &SystemTrayIcon::ChangeVolume, this, &MainWindow::VolumeWheelEvent);
// Windows 7 thumbbar buttons
#ifdef Q_OS_WIN32
@@ -918,7 +915,6 @@ MainWindow::MainWindow(Application *app,
// Analyzer
QObject::connect(ui_->analyzer, &AnalyzerContainer::WheelEvent, this, &MainWindow::VolumeWheelEvent);
ui_->analyzer->SetVisualizationsAction(ui_->action_visualizations);
// Statusbar widgets
ui_->playlist_summary->setMinimumWidth(QFontMetrics(font()).horizontalAdvance(u"WW selected of WW tracks - [ WW:WW ]"_s));
@@ -984,35 +980,34 @@ MainWindow::MainWindow(Application *app,
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdateLastPlayed, &*app_->collection_backend(), &CollectionBackend::UpdateLastPlayed);
QObject::connect(&*app_->lastfm_import(), &LastFMImport::UpdatePlayCount, &*app_->collection_backend(), &CollectionBackend::UpdatePlayCount);
#if !defined(HAVE_AUDIOCD)
#if !defined(HAVE_AUDIOCD) || defined(Q_OS_WIN32)
ui_->action_open_cd->setEnabled(false);
ui_->action_open_cd->setVisible(false);
#endif
// Load settings
qLog(Debug) << "Loading settings";
Settings settings;
settings.beginGroup(MainWindowSettings::kSettingsGroup);
settings_.beginGroup(MainWindowSettings::kSettingsGroup);
// Set last used geometry to position window on the correct monitor
// Set window state only if the window was last maximized
if (settings.contains("geometry")) {
restoreGeometry(settings.value("geometry").toByteArray());
if (settings_.contains("geometry")) {
restoreGeometry(settings_.value("geometry").toByteArray());
}
if (!settings.contains(MainWindowSettings::kSplitterState) || !ui_->splitter->restoreState(settings.value(MainWindowSettings::kSplitterState).toByteArray())) {
if (!settings_.contains(MainWindowSettings::kSplitterState) || !ui_->splitter->restoreState(settings_.value(MainWindowSettings::kSplitterState).toByteArray())) {
ui_->splitter->setSizes(QList<int>() << 20 << (width() - 20));
}
ui_->tabs->setCurrentIndex(settings.value("current_tab", 1).toInt());
ui_->tabs->setCurrentIndex(settings_.value("current_tab", 1).toInt());
FancyTabWidget::Mode default_mode = FancyTabWidget::Mode::LargeSidebar;
FancyTabWidget::Mode tab_mode = static_cast<FancyTabWidget::Mode>(settings.value("tab_mode", static_cast<int>(default_mode)).toInt());
FancyTabWidget::Mode tab_mode = static_cast<FancyTabWidget::Mode>(settings_.value("tab_mode", static_cast<int>(default_mode)).toInt());
if (tab_mode == FancyTabWidget::Mode::None) tab_mode = default_mode;
ui_->tabs->SetMode(tab_mode);
TabSwitched();
file_view_->SetPath(settings.value("file_path", QDir::homePath()).toString());
file_view_->SetPath(settings_.value("file_path", QDir::homePath()).toString());
// Users often collapse one side of the splitter by mistake and don't know how to restore it. This must be set after the state is restored above.
ui_->splitter->setChildrenCollapsible(false);
@@ -1048,20 +1043,20 @@ MainWindow::MainWindow(Application *app,
show();
break;
case BehaviourSettings::StartupBehaviour::Hide:
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible()) {
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible()) {
break;
}
[[fallthrough]];
case BehaviourSettings::StartupBehaviour::Remember:
default:{
was_maximized_ = settings.value(MainWindowSettings::kMaximized, true).toBool();
was_maximized_ = settings_.value(MainWindowSettings::kMaximized, true).toBool();
if (was_maximized_) setWindowState(windowState() | Qt::WindowMaximized);
was_minimized_ = settings.value(MainWindowSettings::kMinimized, false).toBool();
was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool();
if (was_minimized_) setWindowState(windowState() | Qt::WindowMinimized);
if (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !settings.value(MainWindowSettings::kHidden, false).toBool()) {
if (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
show();
}
break;
@@ -1069,7 +1064,7 @@ MainWindow::MainWindow(Application *app,
}
#endif
bool show_sidebar = settings.value(MainWindowSettings::kShowSidebar, true).toBool();
bool show_sidebar = settings_.value(MainWindowSettings::kShowSidebar, true).toBool();
ui_->sidebar_layout->setVisible(show_sidebar);
ui_->action_toggle_show_sidebar->setChecked(show_sidebar);
@@ -1173,13 +1168,13 @@ void MainWindow::ReloadSettings() {
#ifdef Q_OS_MACOS
constexpr bool keeprunning_available = true;
#else
const bool systemtray_available = systemtrayicon_->IsSystemTrayAvailable();
const bool systemtray_available = tray_icon_->IsSystemTrayAvailable();
s.beginGroup(BehaviourSettings::kSettingsGroup);
const bool showtrayicon = s.value(BehaviourSettings::kShowTrayIcon, systemtray_available).toBool();
s.endGroup();
const bool keeprunning_available = systemtray_available && showtrayicon;
if (systemtray_available) {
systemtrayicon_->setVisible(showtrayicon);
tray_icon_->setVisible(showtrayicon);
}
if ((!showtrayicon || !systemtray_available) && !isVisible()) {
show();
@@ -1204,7 +1199,7 @@ void MainWindow::ReloadSettings() {
int iconsize = s.value(AppearanceSettings::kIconSizePlayControlButtons, 32).toInt();
s.endGroup();
systemtrayicon_->SetTrayiconProgress(trayicon_progress);
tray_icon_->SetTrayiconProgress(trayicon_progress);
#ifdef HAVE_DBUS
if (taskbar_progress_ && !taskbar_progress) {
@@ -1226,11 +1221,11 @@ void MainWindow::ReloadSettings() {
ui_->volume->SetEnabled(volume_control);
if (volume_control) {
if (!ui_->action_mute->isVisible()) ui_->action_mute->setVisible(true);
if (!systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(true);
if (!tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(true);
}
else {
if (ui_->action_mute->isVisible()) ui_->action_mute->setVisible(false);
if (systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(false);
if (tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(false);
}
}
@@ -1240,9 +1235,7 @@ void MainWindow::ReloadSettings() {
osd_->ReloadSettings();
s.beginGroup(MainWindowSettings::kSettingsGroup);
album_cover_choice_controller_->search_cover_auto_action()->setChecked(s.value(MainWindowSettings::kSearchForCoverAuto, true).toBool());
s.endGroup();
album_cover_choice_controller_->search_cover_auto_action()->setChecked(settings_.value(MainWindowSettings::kSearchForCoverAuto, true).toBool());
#ifdef HAVE_SUBSONIC
s.beginGroup(SubsonicSettings::kSettingsGroup);
@@ -1359,11 +1352,8 @@ void MainWindow::SaveSettings() {
ui_->playlist->view()->SaveSettings();
app_->scrobbler()->WriteCache();
Settings s;
s.beginGroup(MainWindowSettings::kSettingsGroup);
s.setValue(MainWindowSettings::kShowSidebar, ui_->action_toggle_show_sidebar->isChecked());
s.setValue(MainWindowSettings::kSearchForCoverAuto, album_cover_choice_controller_->search_cover_auto_action()->isChecked());
s.endGroup();
settings_.setValue(MainWindowSettings::kShowSidebar, ui_->action_toggle_show_sidebar->isChecked());
settings_.setValue(MainWindowSettings::kSearchForCoverAuto, album_cover_choice_controller_->search_cover_auto_action()->isChecked());
}
@@ -1387,8 +1377,8 @@ void MainWindow::Exit() {
if (app_->player()->GetState() == EngineBase::State::Playing) {
app_->player()->Stop();
hide();
if (systemtrayicon_->IsSystemTrayAvailable()) {
systemtrayicon_->setVisible(false);
if (tray_icon_->IsSystemTrayAvailable()) {
tray_icon_->setVisible(false);
}
return; // Don't quit the application now: wait for the fadeout finished signal
}
@@ -1445,7 +1435,7 @@ void MainWindow::MediaStopped() {
ui_->action_love->setEnabled(false);
ui_->button_love->setEnabled(false);
systemtrayicon_->LoveStateChanged(false);
tray_icon_->LoveStateChanged(false);
if (track_position_timer_->isActive()) {
track_position_timer_->stop();
@@ -1454,8 +1444,8 @@ void MainWindow::MediaStopped() {
track_slider_timer_->stop();
}
ui_->track_slider->SetStopped();
systemtrayicon_->SetProgress(0);
systemtrayicon_->SetStopped();
tray_icon_->SetProgress(0);
tray_icon_->SetStopped();
#ifdef HAVE_DBUS
if (taskbar_progress_) {
@@ -1487,7 +1477,7 @@ void MainWindow::MediaPaused() {
track_slider_timer_->start();
}
systemtrayicon_->SetPaused();
tray_icon_->SetPaused();
}
@@ -1508,7 +1498,7 @@ void MainWindow::MediaPlaying() {
}
ui_->action_play_pause->setEnabled(enable_play_pause);
ui_->track_slider->SetCanSeek(can_seek);
systemtrayicon_->SetPlaying(enable_play_pause);
tray_icon_->SetPlaying(enable_play_pause);
if (!track_position_timer_->isActive()) {
track_position_timer_->start();
@@ -1525,18 +1515,18 @@ void MainWindow::SendNowPlaying() {
// Send now playing to scrobble services
Playlist *playlist = app_->playlist_manager()->active();
if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->EffectiveMetadata().is_metadata_good()) {
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->EffectiveMetadata());
if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->Metadata().is_metadata_good()) {
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->Metadata());
ui_->action_love->setEnabled(true);
ui_->button_love->setEnabled(true);
systemtrayicon_->LoveStateChanged(true);
tray_icon_->LoveStateChanged(true);
}
}
void MainWindow::VolumeChanged(const uint volume) {
ui_->action_mute->setChecked(volume == 0);
systemtrayicon_->MuteButtonStateChanged(volume == 0);
tray_icon_->MuteButtonStateChanged(volume == 0);
}
void MainWindow::SongChanged(const Song &song) {
@@ -1546,7 +1536,7 @@ void MainWindow::SongChanged(const Song &song) {
song_playing_ = song;
song_ = song;
setWindowTitle(song.PrettyTitleWithArtist());
systemtrayicon_->SetProgress(0);
tray_icon_->SetProgress(0);
#ifdef HAVE_DBUS
if (taskbar_progress_) {
@@ -1572,9 +1562,9 @@ void MainWindow::TrackSkipped(PlaylistItemPtr item) {
// If it was a collection item then we have to increment its skipped count in the database.
if (item && item->IsLocalCollectionItem() && item->EffectiveMetadata().id() != -1) {
if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
Song song = item->EffectiveMetadata();
Song song = item->Metadata();
const qint64 position = app_->player()->engine()->position_nanosec();
const qint64 length = app_->player()->engine()->length_nanosec();
const float percentage = (length == 0 ? 1 : static_cast<float>(position) / static_cast<float>(length));
@@ -1604,35 +1594,23 @@ void MainWindow::ToggleSidebar(const bool checked) {
ui_->sidebar_layout->setVisible(checked);
TabSwitched();
Settings s;
s.beginGroup(MainWindowSettings::kSettingsGroup);
s.setValue(MainWindowSettings::kShowSidebar, checked);
s.endGroup();
settings_.setValue(MainWindowSettings::kShowSidebar, checked);
}
void MainWindow::ToggleSearchCoverAuto(const bool checked) {
Settings s;
s.beginGroup(MainWindowSettings::kSettingsGroup);
s.setValue(MainWindowSettings::kSearchForCoverAuto, checked);
s.endGroup();
settings_.setValue(MainWindowSettings::kSearchForCoverAuto, checked);
}
void MainWindow::SaveGeometry() {
if (!initialized_) return;
Settings s;
s.beginGroup(MainWindowSettings::kSettingsGroup);
s.setValue(MainWindowSettings::kMaximized, isMaximized());
s.setValue(MainWindowSettings::kMinimized, isMinimized());
s.setValue(MainWindowSettings::kHidden, isHidden());
s.setValue(MainWindowSettings::kGeometry, saveGeometry());
s.setValue(MainWindowSettings::kSplitterState, ui_->splitter->saveState());
s.endGroup();
settings_.setValue(MainWindowSettings::kMaximized, isMaximized());
settings_.setValue(MainWindowSettings::kMinimized, isMinimized());
settings_.setValue(MainWindowSettings::kHidden, isHidden());
settings_.setValue(MainWindowSettings::kGeometry, saveGeometry());
settings_.setValue(MainWindowSettings::kSplitterState, ui_->splitter->saveState());
}
@@ -1716,6 +1694,17 @@ void MainWindow::StopAfterCurrent() {
Q_EMIT StopAfterToggled(app_->playlist_manager()->active()->stop_after_current());
}
void MainWindow::showEvent(QShowEvent *e) {
if (error_dialog_ && error_dialog_->isVisible() && error_dialog_->isMinimized()) {
error_dialog_->raise();
error_dialog_->activateWindow();
}
QMainWindow::showEvent(e);
}
void MainWindow::hideEvent(QHideEvent *e) {
// Some window managers don't remember maximized state between
@@ -1730,7 +1719,7 @@ void MainWindow::hideEvent(QHideEvent *e) {
void MainWindow::closeEvent(QCloseEvent *e) {
if (!exit_ && (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !keep_running_)) {
if (!exit_ && (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !keep_running_)) {
Exit();
}
@@ -1738,20 +1727,10 @@ void MainWindow::closeEvent(QCloseEvent *e) {
}
void MainWindow::changeEvent(QEvent *e) {
if (e->type() == QEvent::Show || e->type() == QEvent::WindowStateChange || e->type() == QEvent::WindowActivate) {
CheckShowErrorDialog();
}
QMainWindow::changeEvent(e);
}
void MainWindow::SetHiddenInTray(const bool hidden) {
if (hidden && isVisible()) {
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible() && keep_running_) {
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible() && keep_running_) {
close();
}
else {
@@ -1768,25 +1747,19 @@ void MainWindow::SetHiddenInTray(const bool hidden) {
else {
show();
}
CheckShowErrorDialog();
}
}
void MainWindow::FilePathChanged(const QString &path) {
Settings s;
s.beginGroup(MainWindowSettings::kSettingsGroup);
s.setValue("file_path", path);
s.endGroup();
settings_.setValue("file_path", path);
}
void MainWindow::Seeked(const qint64 microseconds) {
const qint64 position = microseconds / kUsecPerSec;
const qint64 length = app_->player()->GetCurrentItem()->EffectiveMetadata().length_nanosec() / kNsecPerSec;
systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
const qint64 length = app_->player()->GetCurrentItem()->Metadata().length_nanosec() / kNsecPerSec;
tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
#ifdef HAVE_DBUS
if (taskbar_progress_) {
@@ -1801,12 +1774,12 @@ void MainWindow::UpdateTrackPosition() {
PlaylistItemPtr item(app_->player()->GetCurrentItem());
if (!item) return;
const qint64 length = (item->EffectiveMetadata().length_nanosec() / kNsecPerSec);
const qint64 length = (item->Metadata().length_nanosec() / kNsecPerSec);
if (length <= 0) return;
const int position = std::floor(static_cast<float>(app_->player()->engine()->position_nanosec()) / static_cast<float>(kNsecPerSec) + 0.5);
// Update the tray icon every 10 seconds
if (position % 10 == 0) systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
if (position % 10 == 0) tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
#ifdef HAVE_DBUS
if (taskbar_progress_) {
@@ -1815,12 +1788,12 @@ void MainWindow::UpdateTrackPosition() {
#endif
// Send Scrobble
if (app_->scrobbler()->enabled() && item->EffectiveMetadata().is_metadata_good()) {
if (app_->scrobbler()->enabled() && item->Metadata().is_metadata_good()) {
Playlist *playlist = app_->playlist_manager()->active();
if (playlist && !playlist->scrobbled()) {
const qint64 scrobble_point = (playlist->scrobble_point_nanosec() / kNsecPerSec);
if (position >= scrobble_point) {
app_->scrobbler()->Scrobble(item->EffectiveMetadata(), scrobble_point);
app_->scrobbler()->Scrobble(item->Metadata(), scrobble_point);
playlist->set_scrobbled(true);
}
}
@@ -1937,7 +1910,7 @@ void MainWindow::AddToPlaylistFromAction(QAction *action) {
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
if (!item) continue;
items << item;
songs << item->EffectiveMetadata();
songs << item->Metadata();
}
// We're creating a new playlist
@@ -2016,12 +1989,12 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(src_idx.row());
if (!item) continue;
if (item->EffectiveMetadata().url().isLocalFile()) ++local_songs;
if (item->Metadata().url().isLocalFile()) ++local_songs;
if (item->EffectiveMetadata().has_cue()) {
if (item->Metadata().has_cue()) {
cue_selected = true;
}
else if (item->EffectiveMetadata().IsEditable()) {
else if (item->Metadata().IsEditable()) {
++editable;
}
@@ -2059,7 +2032,9 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
playlist_show_in_collection_->setVisible(false);
playlist_copy_to_collection_->setVisible(false);
playlist_move_to_collection_->setVisible(false);
#ifndef Q_OS_WIN32
playlist_copy_to_device_->setVisible(false);
#endif
playlist_organize_->setVisible(false);
playlist_delete_->setVisible(false);
@@ -2122,7 +2097,7 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
// Is it a collection item?
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
if (item && item->IsLocalCollectionItem() && item->EffectiveMetadata().id() != -1) {
if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
playlist_organize_->setVisible(local_songs > 0 && editable > 0 && !cue_selected);
playlist_show_in_collection_->setVisible(true);
playlist_open_in_browser_->setVisible(true);
@@ -2132,7 +2107,9 @@ void MainWindow::PlaylistRightClick(const QPoint global_pos, const QModelIndex &
playlist_move_to_collection_->setVisible(local_songs > 0);
}
#ifndef Q_OS_WIN32
playlist_copy_to_device_->setVisible(local_songs > 0);
#endif
playlist_delete_->setVisible(delete_files_ && local_songs > 0);
@@ -2212,9 +2189,9 @@ void MainWindow::RescanSongs() {
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(source_index.row()));
if (!item) continue;
if (item->IsLocalCollectionItem()) {
songs << item->EffectiveMetadata();
songs << item->Metadata();
}
else if (item->EffectiveMetadata().source() == Song::Source::LocalFile) {
else if (item->Metadata().source() == Song::Source::LocalFile) {
QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
app_->playlist_manager()->current()->ItemReload(persistent_index, item->OriginalMetadata(), false);
}
@@ -2361,9 +2338,7 @@ void MainWindow::EditValue() {
void MainWindow::AddFile() {
// Last used directory
Settings s;
s.beginGroup(MainWindowSettings::kSettingsGroup);
QString directory = s.value("add_media_path", QDir::currentPath()).toString();
QString directory = settings_.value("add_media_path", QDir::currentPath()).toString();
PlaylistParser parser(app_->tagreader_client(), app_->collection_backend());
@@ -2373,7 +2348,7 @@ void MainWindow::AddFile() {
if (filenames.isEmpty()) return;
// Save last used directory
s.setValue("add_media_path", filenames[0]);
settings_.setValue("add_media_path", filenames[0]);
// Convert to URLs
QList<QUrl> urls;
@@ -2391,16 +2366,14 @@ void MainWindow::AddFile() {
void MainWindow::AddFolder() {
// Last used directory
Settings s;
s.beginGroup(MainWindowSettings::kSettingsGroup);
QString directory = s.value("add_folder_path", QDir::currentPath()).toString();
QString directory = settings_.value("add_folder_path", QDir::currentPath()).toString();
// Show dialog
directory = QFileDialog::getExistingDirectory(this, tr("Add folder"), directory);
if (directory.isEmpty()) return;
// Save last used directory
s.setValue("add_folder_path", directory);
settings_.setValue("add_folder_path", directory);
// Add media
MimeData *mimedata = new MimeData;
@@ -2778,6 +2751,7 @@ void MainWindow::MoveFilesToCollection(const QList<QUrl> &urls) {
void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
#ifndef Q_OS_WIN32
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
organize_dialog_->SetCopy(true);
if (organize_dialog_->SetUrls(urls)) {
@@ -2787,6 +2761,9 @@ void MainWindow::CopyFilesToDevice(const QList<QUrl> &urls) {
else {
QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device"));
}
#else
Q_UNUSED(urls);
#endif
}
@@ -2846,7 +2823,7 @@ void MainWindow::PlaylistOpenInBrowser() {
for (const QModelIndex &proxy_index : proxy_indexes) {
const QModelIndex source_index = app_->playlist_manager()->current()->filter()->mapToSource(proxy_index);
if (!source_index.isValid()) continue;
urls << QUrl(source_index.sibling(source_index.row(), static_cast<int>(Playlist::Column::URL)).data().toString());
urls << QUrl(source_index.sibling(source_index.row(), static_cast<int>(Playlist::Column::Filename)).data().toString());
}
Utilities::OpenInFileBrowser(urls);
@@ -2862,7 +2839,7 @@ void MainWindow::PlaylistCopyUrl() {
if (!source_index.isValid()) continue;
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
if (!item) continue;
urls << item->EffectiveUrl();
urls << item->StreamUrl();
}
if (urls.count() > 0) {
@@ -2914,6 +2891,8 @@ void MainWindow::PlaylistSkip() {
void MainWindow::PlaylistCopyToDevice() {
#ifndef Q_OS_WIN32
SongList songs;
const QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
@@ -2938,6 +2917,8 @@ void MainWindow::PlaylistCopyToDevice() {
QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device"));
}
#endif
}
void MainWindow::ChangeCollectionFilterMode(QAction *action) {
@@ -3031,14 +3012,6 @@ void MainWindow::ShowErrorDialog(const QString &message) {
error_dialog_->ShowMessage(message);
}
void MainWindow::CheckShowErrorDialog() {
if (isVisible() && !isMinimized() && error_dialog_ && error_dialog_->isVisible() && !error_dialog_->isActiveWindow()) {
error_dialog_->ShowDialog();
}
}
void MainWindow::CheckFullRescanRevisions() {
int from = app_->database()->startup_schema_version();
@@ -3310,7 +3283,7 @@ void MainWindow::LoveButtonVisibilityChanged(const bool value) {
else
ui_->widget_love->hide();
systemtrayicon_->LoveVisibilityChanged(value);
tray_icon_->LoveVisibilityChanged(value);
}
@@ -3333,7 +3306,7 @@ void MainWindow::Love() {
app_->scrobbler()->Love();
ui_->button_love->setEnabled(false);
ui_->action_love->setEnabled(false);
systemtrayicon_->LoveStateChanged(false);
tray_icon_->LoveStateChanged(false);
}
@@ -3348,10 +3321,10 @@ void MainWindow::PlaylistDelete() {
for (const QModelIndex &proxy_idx : proxy_indexes) {
QModelIndex source_idx = app_->playlist_manager()->current()->filter()->mapToSource(proxy_idx);
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_idx.row());
if (!item || !item->EffectiveMetadata().url().isLocalFile()) continue;
QString filename = item->EffectiveMetadata().url().toLocalFile();
if (!item || !item->Metadata().url().isLocalFile()) continue;
QString filename = item->Metadata().url().toLocalFile();
if (files.contains(filename)) continue;
selected_songs << item->EffectiveMetadata();
selected_songs << item->Metadata();
files << filename;
if (item == app_->player()->GetCurrentItem()) is_current_item = true;
}
@@ -3359,7 +3332,7 @@ void MainWindow::PlaylistDelete() {
if (DeleteConfirmationDialog::warning(files) != QDialogButtonBox::Yes) return;
if (app_->player()->GetState() == EngineBase::State::Playing && app_->playlist_manager()->current() == app_->playlist_manager()->active() && app_->playlist_manager()->current()->rowCount() == selected_songs.count()) {
if (app_->player()->GetState() == EngineBase::State::Playing && app_->playlist_manager()->current()->rowCount() == selected_songs.count()) {
app_->player()->Stop();
}
@@ -3416,24 +3389,3 @@ void MainWindow::FocusSearchField() {
}
}
void MainWindow::ShowVisualizations() {
#ifdef HAVE_VISUALIZATIONS
if (!visualization_) {
visualization_.reset(new VisualizationContainer);
visualization_->SetActions(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_next_track);
connect(&*app_->player(), &Player::Stopped, &*visualization_, &VisualizationContainer::Stopped);
connect(&*app_->player(), &Player::ForceShowOSD, &*visualization_, &VisualizationContainer::SongMetadataChanged);
connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, &*visualization_, &VisualizationContainer::SongMetadataChanged);
visualization_->SetEngine(qobject_cast<GstEngine*>(&*app_->player()->engine()));
}
visualization_->show();
#endif // HAVE_VISUALIZATIONS
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2013-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2013-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
@@ -45,6 +45,7 @@
#include <QImage>
#include <QPixmap>
#include <QTimer>
#include <QSettings>
#include <QtEvents>
#include "includes/scoped_ptr.h"
@@ -52,6 +53,7 @@
#include "includes/lazy.h"
#include "core/platforminterface.h"
#include "core/song.h"
#include "core/settings.h"
#include "core/commandlineoptions.h"
#include "tagreader/tagreaderclient.h"
#include "osd/osdbase.h"
@@ -71,7 +73,9 @@ class CollectionViewContainer;
class CollectionFilter;
class AlbumCoverChoiceController;
class CommandlineOptions;
#ifndef Q_OS_WIN32
class DeviceViewContainer;
#endif
class EditTagDialog;
class Equalizer;
class ErrorDialog;
@@ -97,7 +101,6 @@ class Windows7ThumbBar;
class AddStreamDialog;
class LastFMImportDialog;
class RadioViewContainer;
class VisualizationContainer;
#ifdef HAVE_DISCORD_RPC
namespace discord {
@@ -110,7 +113,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
public:
explicit MainWindow(Application *app,
SharedPtr<SystemTrayIcon> systemtrayicon,
SharedPtr<SystemTrayIcon> tray_icon,
OSDBase *osd,
#ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence,
@@ -123,9 +126,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void CommandlineOptionsReceived(const CommandlineOptions &options);
protected:
void showEvent(QShowEvent *e) override;
void hideEvent(QHideEvent *e) override;
void closeEvent(QCloseEvent *e) override;
void changeEvent(QEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
#ifdef Q_OS_WIN32
bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;
@@ -235,7 +238,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void ShowAboutDialog();
void ShowErrorDialog(const QString &message);
void CheckShowErrorDialog();
void ShowTranscodeDialog();
SettingsDialog *CreateSettingsDialog();
EditTagDialog *CreateEditTagDialog();
@@ -282,9 +284,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
public Q_SLOTS:
void CommandlineOptionsReceived(const QByteArray &string_options);
void Raise();
void ShowVisualizations();
private:
void SaveSettings();
static void ApplyAddBehaviour(const BehaviourSettings::AddBehaviour b, MimeData *mimedata);
@@ -310,12 +312,11 @@ class MainWindow : public QMainWindow, public PlatformInterface {
#endif
Application *app_;
SharedPtr<SystemTrayIcon> systemtrayicon_;
SharedPtr<SystemTrayIcon> tray_icon_;
OSDBase *osd_;
#ifdef HAVE_DISCORD_RPC
discord::RichPresence *discord_rich_presence_;
#endif
Lazy<ErrorDialog> error_dialog_;
Lazy<About> about_dialog_;
Lazy<Console> console_;
Lazy<EditTagDialog> edit_tag_dialog_;
@@ -326,10 +327,13 @@ class MainWindow : public QMainWindow, public PlatformInterface {
ContextView *context_view_;
CollectionViewContainer *collection_view_;
FileView *file_view_;
#ifndef Q_OS_WIN32
DeviceViewContainer *device_view_;
#endif
PlaylistListContainer *playlist_list_;
QueueView *queue_view_;
Lazy<ErrorDialog> error_dialog_;
Lazy<SettingsDialog> settings_dialog_;
Lazy<AlbumCoverManager> cover_manager_;
SharedPtr<Equalizer> equalizer_;
@@ -362,10 +366,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
LastFMImportDialog *lastfm_import_dialog_;
#ifdef HAVE_VISUALIZATIONS
ScopedPtr<VisualizationContainer> visualization_;
#endif
QAction *collection_show_all_;
QAction *collection_show_duplicates_;
QAction *collection_show_untagged_;
@@ -380,7 +380,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
QAction *playlist_move_to_collection_;
QAction *playlist_open_in_browser_;
QAction *playlist_organize_;
#ifndef Q_OS_WIN32
QAction *playlist_copy_to_device_;
#endif
QAction *playlist_delete_;
QAction *playlist_queue_;
QAction *playlist_queue_play_next_;
@@ -394,6 +396,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
QTimer *track_position_timer_;
QTimer *track_slider_timer_;
Settings settings_;
bool keep_running_;
bool playing_widget_;
@@ -417,6 +420,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
bool playlists_loaded_;
bool delete_files_;
std::optional<CommandlineOptions> options_;
};
#endif // MAINWINDOW_H

View File

@@ -13,6 +13,10 @@
<property name="windowTitle">
<string>Strawberry Music Player</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/icons.qrc">
<normaloff>:/icons/128x128/strawberry.png</normaloff>:/icons/128x128/strawberry.png</iconset>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="layout_centralWidget">
<property name="spacing">
@@ -33,7 +37,7 @@
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QWidget" name="sidebar_layout">
<layout class="QVBoxLayout" name="layout_left">
@@ -73,7 +77,7 @@
<item>
<widget class="Line" name="line_6">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
@@ -98,7 +102,7 @@
<item>
<widget class="QFrame" name="player_controls">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
<enum>QFrame::Shape::NoFrame</enum>
</property>
<layout class="QHBoxLayout" name="layout_player_controls">
<property name="spacing">
@@ -163,7 +167,7 @@
</size>
</property>
<property name="popupMode">
<enum>QToolButton::MenuButtonPopup</enum>
<enum>QToolButton::ToolButtonPopupMode::MenuButtonPopup</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
@@ -207,7 +211,7 @@
<item>
<widget class="Line" name="line_love">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
@@ -233,7 +237,7 @@
<item>
<widget class="Line" name="line_buttons">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
@@ -256,10 +260,10 @@
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
<enum>QSizePolicy::Policy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@@ -272,7 +276,7 @@
<item>
<widget class="Line" name="line_volume">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
@@ -288,7 +292,7 @@
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
@@ -322,7 +326,7 @@
<item>
<widget class="Line" name="status_bar_line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
@@ -376,7 +380,7 @@
<item>
<widget class="QLabel" name="playlist_summary">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
@@ -387,7 +391,7 @@
<item>
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
@@ -397,7 +401,7 @@
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
@@ -517,7 +521,6 @@
<addaction name="action_cover_manager"/>
<addaction name="action_equalizer"/>
<addaction name="action_transcoder"/>
<addaction name="action_visualizations"/>
<addaction name="separator"/>
<addaction name="action_update_collection"/>
<addaction name="action_full_collection_scan"/>
@@ -577,7 +580,7 @@
<string>Ctrl+Q</string>
</property>
<property name="menuRole">
<enum>QAction::QuitRole</enum>
<enum>QAction::MenuRole::QuitRole</enum>
</property>
</action>
<action name="action_stop_after_this_track">
@@ -641,7 +644,7 @@
<string>Ctrl+P</string>
</property>
<property name="menuRole">
<enum>QAction::PreferencesRole</enum>
<enum>QAction::MenuRole::PreferencesRole</enum>
</property>
</action>
<action name="action_about_strawberry">
@@ -656,7 +659,7 @@
<string>F1</string>
</property>
<property name="menuRole">
<enum>QAction::AboutRole</enum>
<enum>QAction::MenuRole::AboutRole</enum>
</property>
</action>
<action name="action_shuffle">
@@ -782,7 +785,7 @@
<string>About &amp;Qt</string>
</property>
<property name="menuRole">
<enum>QAction::AboutQtRole</enum>
<enum>QAction::MenuRole::AboutQtRole</enum>
</property>
</action>
<action name="action_mute">
@@ -864,11 +867,6 @@
<string>Import data from last.fm...</string>
</property>
</action>
<action name="action_visualizations">
<property name="text">
<string>Visualizations</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>

View File

@@ -29,7 +29,6 @@
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkInformation>
#include "networkaccessmanager.h"
#include "threadsafenetworkdiskcache.h"
@@ -42,47 +41,31 @@ NetworkAccessManager::NetworkAccessManager(QObject *parent)
setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
setCache(new ThreadSafeNetworkDiskCache(this));
// Handle network state changes after system suspend/resume
// QNetworkInformation provides cross-platform network reachability monitoring in Qt 6
if (QNetworkInformation::loadDefaultBackend()) {
QNetworkInformation *network_info = QNetworkInformation::instance();
if (network_info) {
QObject::connect(network_info, &QNetworkInformation::reachabilityChanged, this, [this](QNetworkInformation::Reachability reachability) {
if (reachability == QNetworkInformation::Reachability::Online) {
// Clear connection cache to force reconnection after network becomes available
// This fixes issues after system suspend/resume
clearConnectionCache();
clearAccessCache();
}
});
}
}
}
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) {
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) {
QByteArray user_agent;
if (network_request.hasRawHeader("User-Agent")) {
user_agent = network_request.header(QNetworkRequest::UserAgentHeader).toByteArray();
if (request.hasRawHeader("User-Agent")) {
user_agent = request.header(QNetworkRequest::UserAgentHeader).toByteArray();
}
else {
user_agent = "Strawberry Music Player";
user_agent = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
}
QNetworkRequest new_network_request(network_request);
new_network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
new_network_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
QNetworkRequest new_request(request);
new_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
new_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
if (op == QNetworkAccessManager::PostOperation && !new_network_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
new_network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
if (op == QNetworkAccessManager::PostOperation && !new_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
new_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
}
// Prefer the cache unless the caller has changed the setting already
if (!network_request.attribute(QNetworkRequest::CacheLoadControlAttribute).isValid()) {
new_network_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
if (request.attribute(QNetworkRequest::CacheLoadControlAttribute).toInt() == QNetworkRequest::PreferNetwork) {
new_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
}
return QNetworkAccessManager::createRequest(op, new_network_request, outgoing_data);
return QNetworkAccessManager::createRequest(op, new_request, outgoingData);
}

View File

@@ -38,7 +38,7 @@ class NetworkAccessManager : public QNetworkAccessManager {
explicit NetworkAccessManager(QObject *parent = nullptr);
protected:
QNetworkReply *createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) override;
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override;
};
#endif // NETWORKACCESSMANAGER_H

View File

@@ -288,10 +288,10 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
bool is_current = false;
bool is_next = false;
if (result.media_url_ == current_item->OriginalUrl()) {
if (result.media_url_ == current_item->Url()) {
is_current = true;
}
else if (has_next_row && next_item->OriginalUrl() == result.media_url_) {
else if (has_next_row && next_item->Url() == result.media_url_) {
is_next = true;
}
else {
@@ -316,8 +316,8 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
Song song;
if (is_current) song = current_item->EffectiveMetadata();
else if (is_next) song = next_item->EffectiveMetadata();
if (is_current) song = current_item->Metadata();
else if (is_next) song = next_item->Metadata();
bool update = false;
@@ -325,7 +325,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
if (
(result.stream_url_.isValid())
&&
(result.stream_url_ != song.effective_url())
(result.stream_url_ != song.url())
)
{
song.set_stream_url(result.stream_url_);
@@ -371,14 +371,14 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
}
if (is_current) {
qLog(Debug) << "Playing song" << current_item->EffectiveMetadata().title() << result.stream_url_ << "position" << play_offset_nanosec_;
qLog(Debug) << "Playing song" << current_item->Metadata().title() << result.stream_url_ << "position" << play_offset_nanosec_;
engine_->Play(result.media_url_, result.stream_url_, pause_, stream_change_type_, song.has_cue(), static_cast<quint64>(song.beginning_nanosec()), song.end_nanosec(), play_offset_nanosec_, song.ebur128_integrated_loudness_lufs());
current_item_ = current_item;
play_offset_nanosec_ = 0;
}
else if (is_next && !current_item->EffectiveMetadata().is_module_music()) {
qLog(Debug) << "Preloading next song" << next_item->EffectiveMetadata().title() << result.stream_url_;
engine_->StartPreloading(next_item->OriginalUrl(), result.stream_url_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec());
else if (is_next && !current_item->Metadata().is_module_music()) {
qLog(Debug) << "Preloading next song" << next_item->Metadata().title() << result.stream_url_;
engine_->StartPreloading(next_item->Url(), result.stream_url_, song.has_cue(), song.beginning_nanosec(), song.end_nanosec());
}
break;
@@ -504,8 +504,8 @@ bool Player::HandleStopAfter(const Playlist::AutoScroll autoscroll) {
void Player::TrackEnded() {
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->EffectiveMetadata().id() != -1) {
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->EffectiveMetadata().id());
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->Metadata().id() != -1) {
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->Metadata().id());
}
if (HandleStopAfter(Playlist::AutoScroll::Maybe)) return;
@@ -554,7 +554,7 @@ void Player::PlayPause(const quint64 offset_nanosec, const Playlist::AutoScroll
void Player::UnPause() {
if (current_item_ && pause_time_.isValid()) {
const Song &song = current_item_->EffectiveMetadata();
const Song &song = current_item_->Metadata();
if (url_handlers_->CanHandle(song.url()) && song.stream_url_can_expire()) {
const qint64 time = QDateTime::currentSecsSinceEpoch() - pause_time_.toSecsSinceEpoch();
if (time >= 30) { // Stream URL might be expired.
@@ -745,7 +745,7 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
Q_EMIT TrackSkipped(current_item_);
}
if (current_item_ && playlist_manager_->active()->has_item_at(index) && current_item_->EffectiveMetadata().IsOnSameAlbum(playlist_manager_->active()->item_at(index)->EffectiveMetadata())) {
if (current_item_ && playlist_manager_->active()->has_item_at(index) && current_item_->Metadata().IsOnSameAlbum(playlist_manager_->active()->item_at(index)->Metadata())) {
change |= EngineBase::TrackChangeType::SameAlbum;
}
@@ -758,7 +758,7 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
}
current_item_ = playlist_manager_->active()->current_item();
const QUrl url = current_item_->EffectiveUrl();
const QUrl url = current_item_->StreamUrl();
if (url_handlers_->CanHandle(url)) {
// It's already loading
@@ -773,8 +773,8 @@ void Player::PlayAt(const int index, const bool pause, const quint64 offset_nano
HandleLoadResult(url_handler->StartLoading(url));
}
else {
qLog(Debug) << "Playing song" << current_item_->EffectiveMetadata().title() << url << "position" << offset_nanosec;
engine_->Play(current_item_->OriginalUrl(), url, pause, change, current_item_->EffectiveMetadata().has_cue(), static_cast<quint64>(current_item_->effective_beginning_nanosec()), current_item_->effective_end_nanosec(), offset_nanosec, current_item_->EffectiveMetadata().ebur128_integrated_loudness_lufs());
qLog(Debug) << "Playing song" << current_item_->Metadata().title() << url << "position" << offset_nanosec;
engine_->Play(current_item_->Url(), url, pause, change, current_item_->Metadata().has_cue(), static_cast<quint64>(current_item_->effective_beginning_nanosec()), current_item_->effective_end_nanosec(), offset_nanosec, current_item_->effective_ebur128_integrated_loudness_lufs());
}
}
@@ -823,8 +823,8 @@ void Player::EngineMetadataReceived(const EngineMetadata &engine_metadata) {
const int current_row = playlist_manager_->active()->current_row();
if (current_row != -1) {
PlaylistItemPtr item = playlist_manager_->active()->current_item();
if (item && engine_metadata.media_url == item->OriginalUrl()) {
Song song = item->EffectiveMetadata();
if (item && engine_metadata.media_url == item->Url()) {
Song song = item->Metadata();
song.MergeFromEngineMetadata(engine_metadata);
playlist_manager_->active()->UpdateItemMetadata(current_row, item, song, true);
return;
@@ -836,8 +836,8 @@ void Player::EngineMetadataReceived(const EngineMetadata &engine_metadata) {
const int next_row = playlist_manager_->active()->next_row();
if (next_row != -1) {
PlaylistItemPtr next_item = playlist_manager_->active()->item_at(next_row);
if (engine_metadata.media_url == next_item->OriginalUrl()) {
Song song = next_item->EffectiveMetadata();
if (engine_metadata.media_url == next_item->Url()) {
Song song = next_item->Metadata();
song.MergeFromEngineMetadata(engine_metadata);
playlist_manager_->active()->UpdateItemMetadata(next_row, next_item, song, true);
}
@@ -905,11 +905,11 @@ void Player::PlayWithPause(const quint64 offset_nanosec) {
}
void Player::ShowOSD() {
if (current_item_) Q_EMIT ForceShowOSD(current_item_->EffectiveMetadata(), false);
if (current_item_) Q_EMIT ForceShowOSD(current_item_->Metadata(), false);
}
void Player::TogglePrettyOSD() {
if (current_item_) Q_EMIT ForceShowOSD(current_item_->EffectiveMetadata(), true);
if (current_item_) Q_EMIT ForceShowOSD(current_item_->Metadata(), true);
}
void Player::TrackAboutToEnd() {
@@ -932,7 +932,7 @@ void Player::TrackAboutToEnd() {
// If the next track is on the same album (or same cue file),
// and the user doesn't want to crossfade between tracks on the same album, then don't do this automatic crossfading.
if (engine_->crossfade_same_album() || !has_next_row || !next_item || !current_item_->EffectiveMetadata().IsOnSameAlbum(next_item->EffectiveMetadata())) {
if (engine_->crossfade_same_album() || !has_next_row || !next_item || !current_item_->Metadata().IsOnSameAlbum(next_item->Metadata())) {
TrackEnded();
return;
}
@@ -941,7 +941,7 @@ void Player::TrackAboutToEnd() {
// Crossfade is off, so start preloading the next track, so we don't get a gap between songs.
if (!has_next_row || !next_item) return;
QUrl url = next_item->EffectiveUrl();
QUrl url = next_item->StreamUrl();
// Get the actual track URL rather than the stream URL.
if (url_handlers_->CanHandle(url)) {
@@ -961,20 +961,20 @@ void Player::TrackAboutToEnd() {
case UrlHandler::LoadResult::Type::TrackAvailable:
qLog(Debug) << "URL handler for" << result.media_url_ << "returned" << result.stream_url_;
url = result.stream_url_;
Song song = next_item->EffectiveMetadata();
Song song = next_item->Metadata();
song.set_stream_url(url);
next_item->SetStreamMetadata(song);
next_item->SetTemporaryMetadata(song);
break;
}
}
// Preloading any format while currently playing module music is broken in GStreamer.
// See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/769
if (current_item_ && current_item_->EffectiveMetadata().is_module_music()) {
if (current_item_ && current_item_->Metadata().is_module_music()) {
return;
}
engine_->StartPreloading(next_item->OriginalUrl(), url, next_item->EffectiveMetadata().has_cue(), next_item->effective_beginning_nanosec(), next_item->effective_end_nanosec());
engine_->StartPreloading(next_item->Url(), url, next_item->Metadata().has_cue(), next_item->effective_beginning_nanosec(), next_item->effective_end_nanosec());
}

View File

@@ -121,8 +121,8 @@ class Player : public PlayerInterface {
void PlayPlaylistInternal(const EngineBase::TrackChangeFlags, const Playlist::AutoScroll autoscroll, const QString &playlist_name);
void FatalError();
void ValidSongRequested(const QUrl &url);
void InvalidSongRequested(const QUrl &url);
void ValidSongRequested(const QUrl&);
void InvalidSongRequested(const QUrl&);
void HandleLoadResult(const UrlHandler::LoadResult &result);

View File

@@ -40,6 +40,7 @@ class QtFSListener : public FileSystemWatcherInterface {
private:
QFileSystemWatcher watcher_;
};
#endif // QTFSLISTENER_H

View File

@@ -28,7 +28,6 @@ class ScopedNSAutoreleasePool {
// Only use then when you're certain the items currently in the pool are
// no longer needed.
void Recycle();
private:
NSAutoreleasePool* autorelease_pool_;

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -68,13 +68,9 @@
using namespace Qt::Literals::StringLiterals;
const QStringList Song::kColumns = QStringList() << u"title"_s
<< u"titlesort"_s
<< u"album"_s
<< u"albumsort"_s
<< u"artist"_s
<< u"artistsort"_s
<< u"albumartist"_s
<< u"albumartistsort"_s
<< u"track"_s
<< u"disc"_s
<< u"year"_s
@@ -82,9 +78,7 @@ const QStringList Song::kColumns = QStringList() << u"title"_s
<< u"genre"_s
<< u"compilation"_s
<< u"composer"_s
<< u"composersort"_s
<< u"performer"_s
<< u"performersort"_s
<< u"grouping"_s
<< u"comment"_s
<< u"lyrics"_s
@@ -132,9 +126,6 @@ const QStringList Song::kColumns = QStringList() << u"title"_s
<< u"cue_path"_s
<< u"rating"_s
<< u"bpm"_s
<< u"mood"_s
<< u"initial_key"_s
<< u"acoustid_id"_s
<< u"acoustid_fingerprint"_s
@@ -243,7 +234,6 @@ const QStringList Song::kAcceptedExtensions = QStringList() << u"wav"_s
<< u"tta"_s
<< u"dsf"_s
<< u"dsd"_s
<< u"webm"_s
<< u"ac3"_s
<< u"dts"_s
<< u"spc"_s
@@ -271,13 +261,9 @@ struct Song::Private : public QSharedData {
bool valid_;
QString title_;
QString titlesort_;
QString album_;
QString albumsort_;
QString artist_;
QString artistsort_;
QString albumartist_;
QString albumartistsort_;
int track_;
int disc_;
int year_;
@@ -285,9 +271,7 @@ struct Song::Private : public QSharedData {
QString genre_;
bool compilation_; // From the file tag
QString composer_;
QString composersort_;
QString performer_;
QString performersort_;
QString grouping_;
QString comment_;
QString lyrics_;
@@ -332,9 +316,6 @@ struct Song::Private : public QSharedData {
QString cue_path_; // If the song has a CUE, this contains it's path.
float rating_; // Database rating, initial rating read from tag.
float bpm_;
QString mood_;
QString initial_key_;
QString acoustid_id_;
QString acoustid_fingerprint_;
@@ -353,12 +334,15 @@ struct Song::Private : public QSharedData {
std::optional<double> ebur128_integrated_loudness_lufs_;
std::optional<double> ebur128_loudness_range_lu_;
int id3v2_version_; // ID3v2 tag version (3 or 4), 0 if not applicable or unknown
bool init_from_file_; // Whether this song was loaded from a file using taglib.
bool suspicious_tags_; // Whether our encoding guesser thinks these tags might be incorrectly encoded.
QUrl stream_url_; // Temporary stream URL set by the URL handler.
QString title_sortable_;
QString album_sortable_;
QString artist_sortable_;
QString albumartist_sortable_;
QUrl stream_url_; // Temporary stream url set by the URL handler.
};
@@ -400,9 +384,6 @@ Song::Private::Private(const Source source)
art_unset_(false),
rating_(-1),
bpm_(-1),
id3v2_version_(0),
init_from_file_(false),
suspicious_tags_(false)
@@ -430,13 +411,9 @@ int Song::id() const { return d->id_; }
bool Song::is_valid() const { return d->valid_; }
const QString &Song::title() const { return d->title_; }
const QString &Song::titlesort() const { return d->titlesort_; }
const QString &Song::album() const { return d->album_; }
const QString &Song::albumsort() const { return d->albumsort_; }
const QString &Song::artist() const { return d->artist_; }
const QString &Song::artistsort() const { return d->artistsort_; }
const QString &Song::albumartist() const { return d->albumartist_; }
const QString &Song::albumartistsort() const { return d->albumartistsort_; }
int Song::track() const { return d->track_; }
int Song::disc() const { return d->disc_; }
int Song::year() const { return d->year_; }
@@ -444,9 +421,7 @@ int Song::originalyear() const { return d->originalyear_; }
const QString &Song::genre() const { return d->genre_; }
bool Song::compilation() const { return d->compilation_; }
const QString &Song::composer() const { return d->composer_; }
const QString &Song::composersort() const { return d->composersort_; }
const QString &Song::performer() const { return d->performer_; }
const QString &Song::performersort() const { return d->performersort_; }
const QString &Song::grouping() const { return d->grouping_; }
const QString &Song::comment() const { return d->comment_; }
const QString &Song::lyrics() const { return d->lyrics_; }
@@ -493,9 +468,6 @@ bool Song::art_unset() const { return d->art_unset_; }
const QString &Song::cue_path() const { return d->cue_path_; }
float Song::rating() const { return d->rating_; }
float Song::bpm() const { return d->bpm_; }
const QString &Song::mood() const { return d->mood_; }
const QString &Song::initial_key() const { return d->initial_key_; }
const QString &Song::acoustid_id() const { return d->acoustid_id_; }
const QString &Song::acoustid_fingerprint() const { return d->acoustid_fingerprint_; }
@@ -514,8 +486,6 @@ const QString &Song::musicbrainz_work_id() const { return d->musicbrainz_work_id
std::optional<double> Song::ebur128_integrated_loudness_lufs() const { return d->ebur128_integrated_loudness_lufs_; }
std::optional<double> Song::ebur128_loudness_range_lu() const { return d->ebur128_loudness_range_lu_; }
int Song::id3v2_version() const { return d->id3v2_version_; }
QString *Song::mutable_title() { return &d->title_; }
QString *Song::mutable_album() { return &d->album_; }
QString *Song::mutable_artist() { return &d->artist_; }
@@ -541,19 +511,20 @@ QString *Song::mutable_musicbrainz_work_id() { return &d->musicbrainz_work_id_;
bool Song::init_from_file() const { return d->init_from_file_; }
const QString &Song::title_sortable() const { return d->title_sortable_; }
const QString &Song::album_sortable() const { return d->album_sortable_; }
const QString &Song::artist_sortable() const { return d->artist_sortable_; }
const QString &Song::albumartist_sortable() const { return d->albumartist_sortable_; }
const QUrl &Song::stream_url() const { return d->stream_url_; }
void Song::set_id(const int id) { d->id_ = id; }
void Song::set_valid(const bool v) { d->valid_ = v; }
void Song::set_title(const QString &v) { d->title_ = v; }
void Song::set_titlesort(const QString &v) { d->titlesort_ = v; }
void Song::set_album(const QString &v) { d->album_ = v; }
void Song::set_albumsort(const QString &v) { d->albumsort_ = v; }
void Song::set_artist(const QString &v) { d->artist_ = v; }
void Song::set_artistsort(const QString &v) { d->artistsort_ = v; }
void Song::set_albumartist(const QString &v) { d->albumartist_ = v; }
void Song::set_albumartistsort(const QString &v) { d->albumartistsort_ = v; }
void Song::set_title(const QString &v) { d->title_sortable_ = sortable(v); d->title_ = v; }
void Song::set_album(const QString &v) { d->album_sortable_ = sortable(v); d->album_ = v; }
void Song::set_artist(const QString &v) { d->artist_sortable_ = sortable(v); d->artist_ = v; }
void Song::set_albumartist(const QString &v) { d->albumartist_sortable_ = sortable(v); d->albumartist_ = v; }
void Song::set_track(const int v) { d->track_ = v; }
void Song::set_disc(const int v) { d->disc_ = v; }
void Song::set_year(const int v) { d->year_ = v; }
@@ -561,9 +532,7 @@ void Song::set_originalyear(const int v) { d->originalyear_ = v; }
void Song::set_genre(const QString &v) { d->genre_ = v; }
void Song::set_compilation(const bool v) { d->compilation_ = v; }
void Song::set_composer(const QString &v) { d->composer_ = v; }
void Song::set_composersort(const QString &v) { d->composersort_ = v; }
void Song::set_performer(const QString &v) { d->performer_ = v; }
void Song::set_performersort(const QString &v) { d->performersort_ = v; }
void Song::set_grouping(const QString &v) { d->grouping_ = v; }
void Song::set_comment(const QString &v) { d->comment_ = v; }
void Song::set_lyrics(const QString &v) { d->lyrics_ = v; }
@@ -609,9 +578,6 @@ void Song::set_art_unset(const bool v) { d->art_unset_ = v; }
void Song::set_cue_path(const QString &v) { d->cue_path_ = v; }
void Song::set_rating(const float v) { d->rating_ = v; }
void Song::set_bpm(const float v) { d->bpm_ = v; }
void Song::set_mood(const QString &v) { d->mood_ = v; }
void Song::set_initial_key(const QString &v) { d->initial_key_ = v; }
void Song::set_acoustid_id(const QString &v) { d->acoustid_id_ = v; }
void Song::set_acoustid_fingerprint(const QString &v) { d->acoustid_fingerprint_ = v; }
@@ -630,25 +596,44 @@ void Song::set_musicbrainz_work_id(const QString &v) { d->musicbrainz_work_id_ =
void Song::set_ebur128_integrated_loudness_lufs(const std::optional<double> v) { d->ebur128_integrated_loudness_lufs_ = v; }
void Song::set_ebur128_loudness_range_lu(const std::optional<double> v) { d->ebur128_loudness_range_lu_ = v; }
void Song::set_id3v2_version(const int v) { d->id3v2_version_ = v; }
void Song::set_init_from_file(const bool v) { d->init_from_file_ = v; }
void Song::set_stream_url(const QUrl &v) { d->stream_url_ = v; }
void Song::set_title(const TagLib::String &v) { d->title_ = TagLibStringToQString(v); }
void Song::set_titlesort(const TagLib::String &v) { d->titlesort_ = TagLibStringToQString(v); }
void Song::set_album(const TagLib::String &v) { d->album_ = TagLibStringToQString(v); }
void Song::set_albumsort(const TagLib::String &v) { d->albumsort_ = TagLibStringToQString(v); }
void Song::set_artist(const TagLib::String &v) { d->artist_ = TagLibStringToQString(v); }
void Song::set_artistsort(const TagLib::String &v) { d->artistsort_ = TagLibStringToQString(v); }
void Song::set_albumartist(const TagLib::String &v) { d->albumartist_ = TagLibStringToQString(v); }
void Song::set_albumartistsort(const TagLib::String &v) { d->albumartistsort_ = TagLibStringToQString(v); }
void Song::set_title(const TagLib::String &v) {
const QString title = TagLibStringToQString(v);
d->title_sortable_ = sortable(title);
d->title_ = title;
}
void Song::set_album(const TagLib::String &v) {
const QString album = TagLibStringToQString(v);
d->album_sortable_ = sortable(album);
d->album_ = album;
}
void Song::set_artist(const TagLib::String &v) {
const QString artist = TagLibStringToQString(v);
d->artist_sortable_ = sortable(artist);
d->artist_ = artist;
}
void Song::set_albumartist(const TagLib::String &v) {
const QString albumartist = TagLibStringToQString(v);
d->albumartist_sortable_ = sortable(albumartist);
d->albumartist_ = albumartist;
}
void Song::set_genre(const TagLib::String &v) { d->genre_ = TagLibStringToQString(v); }
void Song::set_composer(const TagLib::String &v) { d->composer_ = TagLibStringToQString(v); }
void Song::set_composersort(const TagLib::String &v) { d->composersort_ = TagLibStringToQString(v); }
void Song::set_performer(const TagLib::String &v) { d->performer_ = TagLibStringToQString(v); }
void Song::set_performersort(const TagLib::String &v) { d->performersort_ = TagLibStringToQString(v); }
void Song::set_grouping(const TagLib::String &v) { d->grouping_ = TagLibStringToQString(v); }
void Song::set_comment(const TagLib::String &v) { d->comment_ = TagLibStringToQString(v); }
void Song::set_lyrics(const TagLib::String &v) { d->lyrics_ = TagLibStringToQString(v); }
@@ -667,21 +652,14 @@ void Song::set_musicbrainz_track_id(const TagLib::String &v) { d->musicbrainz_tr
void Song::set_musicbrainz_disc_id(const TagLib::String &v) { d->musicbrainz_disc_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); }
void Song::set_musicbrainz_release_group_id(const TagLib::String &v) { d->musicbrainz_release_group_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); }
void Song::set_musicbrainz_work_id(const TagLib::String &v) { d->musicbrainz_work_id_ = TagLibStringToQString(v).remove(u' ').replace(u';', u'/'); }
void Song::set_mood(const TagLib::String &v) { d->mood_ = TagLibStringToQString(v); }
void Song::set_initial_key(const TagLib::String &v) { d->initial_key_ = TagLibStringToQString(v); }
const QUrl &Song::effective_url() const { return !d->stream_url_.isEmpty() && d->stream_url_.isValid() ? d->stream_url_ : d->url_; }
const QString &Song::effective_titlesort() const { return d->titlesort_.isEmpty() ? d->title_ : d->titlesort_; }
const QUrl &Song::effective_stream_url() const { return !d->stream_url_.isEmpty() && d->stream_url_.isValid() ? d->stream_url_ : d->url_; }
const QString &Song::effective_albumartist() const { return d->albumartist_.isEmpty() ? d->artist_ : d->albumartist_; }
const QString &Song::effective_albumartistsort() const { return !d->albumartistsort_.isEmpty() ? d->albumartistsort_ : !d->albumartist_.isEmpty() ? d->albumartist_ : effective_artistsort(); }
const QString &Song::effective_artistsort() const { return d->artistsort_.isEmpty() ? d->artist_ : d->artistsort_; }
const QString &Song::effective_albumartist_sortable() const { return d->albumartist_.isEmpty() ? d->artist_sortable_ : d->albumartist_sortable_; }
const QString &Song::effective_album() const { return d->album_.isEmpty() ? d->title_ : d->album_; }
const QString &Song::effective_albumsort() const { return d->albumsort_.isEmpty() ? d->album_ : d->albumsort_; }
const QString &Song::effective_composersort() const { return d->composersort_.isEmpty() ? d->composer_ : d->composersort_; }
const QString &Song::effective_performersort() const { return d->performersort_.isEmpty() ? d->performer_ : d->performersort_; }
int Song::effective_originalyear() const { return d->originalyear_ < 0 ? d->year_ : d->originalyear_; }
const QString &Song::playlist_effective_albumartist() const { return is_compilation() ? d->albumartist_ : effective_albumartist(); }
const QString &Song::playlist_effective_albumartistsort() const { return is_compilation() ? (!d->albumartistsort_.isEmpty() ? d->albumartistsort_ : d->albumartist_) : effective_albumartistsort(); }
const QString &Song::playlist_albumartist() const { return is_compilation() ? d->albumartist_ : effective_albumartist(); }
const QString &Song::playlist_albumartist_sortable() const { return is_compilation() ? d->albumartist_sortable_ : effective_albumartist_sortable(); }
bool Song::is_metadata_good() const { return !d->url_.isEmpty() && !d->artist_.isEmpty() && !d->title_.isEmpty(); }
bool Song::is_local_collection_song() const { return d->source_ == Source::Collection; }
@@ -804,31 +782,6 @@ bool Song::lyrics_supported() const {
return additional_tags_supported() || d->filetype_ == FileType::ASF;
}
bool Song::albumartistsort_supported() const {
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::OggOpus || d->filetype_ == FileType::MPEG;
}
bool Song::albumsort_supported() const {
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::OggOpus || d->filetype_ == FileType::MPEG;
}
bool Song::artistsort_supported() const {
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::OggOpus || d->filetype_ == FileType::MPEG;
}
bool Song::composersort_supported() const {
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::OggOpus || d->filetype_ == FileType::MPEG;
}
bool Song::performersort_supported() const {
// Performer sort is a rare custom field even in vorbis comments, no write support in MPEG formats
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis;
}
bool Song::titlesort_supported() const {
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || d->filetype_ == FileType::OggOpus || d->filetype_ == FileType::MPEG;
}
bool Song::save_embedded_cover_supported(const FileType filetype) {
return filetype == FileType::FLAC ||
@@ -841,8 +794,19 @@ bool Song::save_embedded_cover_supported(const FileType filetype) {
}
bool Song::id3v2_tags_supported() const {
return d->filetype_ == FileType::MPEG || d->filetype_ == FileType::WAV || d->filetype_ == FileType::AIFF;
QString Song::sortable(const QString &v) {
QString copy = v.toLower();
for (const auto &i : kArticles) {
if (copy.startsWith(i)) {
qint64 ilen = i.length();
return copy.right(copy.length() - ilen) + u", "_s + copy.left(ilen - 1);
}
}
return copy;
}
int Song::ColumnIndex(const QString &field) {
@@ -959,31 +923,12 @@ bool Song::IsEditable() const {
return d->valid_ && d->url_.isValid() && ((d->url_.isLocalFile() && write_tags_supported() && !has_cue()) || d->source_ == Source::Stream);
}
bool Song::IsFileInfoEqual(const Song &other) const {
return d->beginning_ == other.d->beginning_ &&
d->end_ == other.d->end_ &&
d->url_ == other.d->url_ &&
d->basefilename_ == other.d->basefilename_ &&
d->filetype_ == other.d->filetype_ &&
d->filesize_ == other.d->filesize_ &&
d->mtime_ == other.d->mtime_ &&
d->ctime_ == other.d->ctime_ &&
d->mtime_ == other.d->mtime_ &&
d->stream_url_ == other.d->stream_url_;
}
bool Song::IsMetadataEqual(const Song &other) const {
return d->title_ == other.d->title_ &&
d->titlesort_ == other.d->titlesort_ &&
d->album_ == other.d->album_ &&
d->albumsort_ == other.d->albumsort_ &&
d->artist_ == other.d->artist_ &&
d->artistsort_ == other.d->artistsort_ &&
d->albumartist_ == other.d->albumartist_ &&
d->albumartistsort_ == other.d->albumartistsort_ &&
d->track_ == other.d->track_ &&
d->disc_ == other.d->disc_ &&
d->year_ == other.d->year_ &&
@@ -991,9 +936,7 @@ bool Song::IsMetadataEqual(const Song &other) const {
d->genre_ == other.d->genre_ &&
d->compilation_ == other.d->compilation_ &&
d->composer_ == other.d->composer_ &&
d->composersort_ == other.d->composersort_ &&
d->performer_ == other.d->performer_ &&
d->performersort_ == other.d->performersort_ &&
d->grouping_ == other.d->grouping_ &&
d->comment_ == other.d->comment_ &&
d->lyrics_ == other.d->lyrics_ &&
@@ -1005,9 +948,6 @@ bool Song::IsMetadataEqual(const Song &other) const {
d->bitrate_ == other.d->bitrate_ &&
d->samplerate_ == other.d->samplerate_ &&
d->bitdepth_ == other.d->bitdepth_ &&
d->bpm_ == other.d->bpm_ &&
d->mood_ == other.d->mood_ &&
d->initial_key_ == other.d->initial_key_ &&
d->cue_path_ == other.d->cue_path_;
}
@@ -1068,23 +1008,6 @@ bool Song::IsArtEqual(const Song &other) const {
}
bool Song::IsCompilationEqual(const Song &other) const {
return d->compilation_ == other.d->compilation_ &&
d->compilation_detected_ == other.d->compilation_detected_ &&
d->compilation_on_ == other.d->compilation_on_ &&
d->compilation_off_ == other.d->compilation_off_;
}
bool Song::IsSettingsEqual(const Song &other) const {
return d->source_ == other.d->source_ &&
d->directory_id_ == other.d->directory_id_ &&
d->unavailable_ == other.d->unavailable_;
}
bool Song::IsAllMetadataEqual(const Song &other) const {
return IsMetadataEqual(other) &&
@@ -1092,18 +1015,7 @@ bool Song::IsAllMetadataEqual(const Song &other) const {
IsRatingEqual(other) &&
IsAcoustIdEqual(other) &&
IsMusicBrainzEqual(other) &&
IsArtEqual(other) &&
IsEBUR128Equal(other);
}
bool Song::IsEqual(const Song &other) const {
return IsFileInfoEqual(other) &&
IsSettingsEqual(other) &&
IsAllMetadataEqual(other) &&
IsFingerprintEqual(other) &&
IsCompilationEqual(other);
IsArtEqual(other);
}
@@ -1227,22 +1139,6 @@ QIcon Song::IconForSource(const Source source) {
}
// Convert a source to a music service domain name, for ListenBrainz.
// See the "Music service names" note on https://listenbrainz.readthedocs.io/en/latest/users/json.html.
QString Song::DomainForSource(const Source source) {
switch (source) {
case Song::Source::Tidal: return u"tidal.com"_s;
case Song::Source::Qobuz: return u"qobuz.com"_s;
case Song::Source::SomaFM: return u"somafm.com"_s;
case Song::Source::RadioParadise: return u"radioparadise.com"_s;
case Song::Source::Spotify: return u"spotify.com"_s;
default: return QString();
}
}
QString Song::TextForFiletype(const FileType filetype) {
switch (filetype) {
@@ -1270,7 +1166,6 @@ QString Song::TextForFiletype(const FileType filetype) {
case FileType::CDDA: return u"CDDA"_s;
case FileType::SPC: return u"SNES SPC700"_s;
case FileType::VGM: return u"VGM"_s;
case FileType::ALAC: return u"ALAC"_s;
case FileType::Stream: return u"Stream"_s;
case FileType::Unknown:
default: return QObject::tr("Unknown");
@@ -1303,7 +1198,6 @@ QString Song::ExtensionForFiletype(const FileType filetype) {
case FileType::IT: return u"it"_s;
case FileType::SPC: return u"spc"_s;
case FileType::VGM: return u"vgm"_s;
case FileType::ALAC: return u"m4a"_s;
case FileType::Unknown:
default: return u"dat"_s;
}
@@ -1336,30 +1230,12 @@ QIcon Song::IconForFiletype(const FileType filetype) {
case FileType::IT: return IconLoader::Load(u"it"_s);
case FileType::CDDA: return IconLoader::Load(u"cd"_s);
case FileType::Stream: return IconLoader::Load(u"applications-internet"_s);
case FileType::ALAC: return IconLoader::Load(u"alac"_s);
case FileType::Unknown:
default: return IconLoader::Load(u"edit-delete"_s);
}
}
// Get a URL usable for sharing this song with another user.
// This is only applicable when streaming from a streaming service, since we can't link to local content.
// Returns a web URL which points to the current streaming track or live stream, or an empty string if that is not applicable.
QString Song::ShareURL() const {
switch (source()) {
case Song::Source::Stream:
case Song::Source::SomaFM: return url().toString();
case Song::Source::Tidal: return "https://tidal.com/track/%1"_L1.arg(song_id());
case Song::Source::Qobuz: return "https://open.qobuz.com/track/%1"_L1.arg(song_id());
case Song::Source::Spotify: return "https://open.spotify.com/track/%1"_L1.arg(song_id());
default: return QString();
}
}
bool Song::IsFileLossless() const {
switch (filetype()) {
@@ -1374,7 +1250,6 @@ bool Song::IsFileLossless() const {
case FileType::TrueAudio:
case FileType::PCM:
case FileType::CDDA:
case FileType::ALAC:
return true;
default:
return false;
@@ -1404,7 +1279,6 @@ Song::FileType Song::FiletypeByMimetype(const QString &mimetype) {
if (mimetype.compare("audio/x-s3m"_L1, Qt::CaseInsensitive) == 0) return FileType::S3M;
if (mimetype.compare("audio/x-spc"_L1, Qt::CaseInsensitive) == 0) return FileType::SPC;
if (mimetype.compare("audio/x-vgm"_L1, Qt::CaseInsensitive) == 0) return FileType::VGM;
if (mimetype.compare("audio/x-alac"_L1, Qt::CaseInsensitive) == 0) return FileType::ALAC;
return FileType::Unknown;
@@ -1432,7 +1306,6 @@ Song::FileType Song::FiletypeByDescription(const QString &text) {
if (text.compare("Module Music Format (MOD)"_L1, Qt::CaseInsensitive) == 0) return FileType::S3M;
if (text.compare("SNES SPC700"_L1, Qt::CaseInsensitive) == 0) return FileType::SPC;
if (text.compare("VGM"_L1, Qt::CaseInsensitive) == 0) return FileType::VGM;
if (text.compare("Apple Lossless Audio Codec (ALAC)"_L1, Qt::CaseInsensitive) == 0) return FileType::ALAC;
return FileType::Unknown;
@@ -1543,13 +1416,9 @@ void Song::InitFromQuery(const QSqlRecord &r, const bool reliable_metadata, cons
d->id_ = SqlHelper::ValueToInt(r, ColumnIndex(u"ROWID"_s) + col);
set_title(SqlHelper::ValueToString(r, ColumnIndex(u"title"_s) + col));
set_titlesort(SqlHelper::ValueToString(r, ColumnIndex(u"titlesort"_s) + col));
set_album(SqlHelper::ValueToString(r, ColumnIndex(u"album"_s) + col));
set_albumsort(SqlHelper::ValueToString(r, ColumnIndex(u"albumsort"_s) + col));
set_artist(SqlHelper::ValueToString(r, ColumnIndex(u"artist"_s) + col));
set_artistsort(SqlHelper::ValueToString(r, ColumnIndex(u"artistsort"_s) + col));
set_albumartist(SqlHelper::ValueToString(r, ColumnIndex(u"albumartist"_s) + col));
set_albumartistsort(SqlHelper::ValueToString(r, ColumnIndex(u"albumartistsort"_s) + col));
d->track_ = SqlHelper::ValueToInt(r, ColumnIndex(u"track"_s) + col);
d->disc_ = SqlHelper::ValueToInt(r, ColumnIndex(u"disc"_s) + col);
d->year_ = SqlHelper::ValueToInt(r, ColumnIndex(u"year"_s) + col);
@@ -1557,9 +1426,7 @@ void Song::InitFromQuery(const QSqlRecord &r, const bool reliable_metadata, cons
d->genre_ = SqlHelper::ValueToString(r, ColumnIndex(u"genre"_s) + col);
d->compilation_ = r.value(ColumnIndex(u"compilation"_s) + col).toBool();
d->composer_ = SqlHelper::ValueToString(r, ColumnIndex(u"composer"_s) + col);
d->composersort_ = SqlHelper::ValueToString(r, ColumnIndex(u"composersort"_s) + col);
d->performer_ = SqlHelper::ValueToString(r, ColumnIndex(u"performer"_s) + col);
d->performersort_ = SqlHelper::ValueToString(r, ColumnIndex(u"performersort"_s) + col);
d->grouping_ = SqlHelper::ValueToString(r, ColumnIndex(u"grouping"_s) + col);
d->comment_ = SqlHelper::ValueToString(r, ColumnIndex(u"comment"_s) + col);
d->lyrics_ = SqlHelper::ValueToString(r, ColumnIndex(u"lyrics"_s) + col);
@@ -1601,11 +1468,7 @@ void Song::InitFromQuery(const QSqlRecord &r, const bool reliable_metadata, cons
d->art_unset_ = SqlHelper::ValueToBool(r, ColumnIndex(u"art_unset"_s) + col);
d->cue_path_ = SqlHelper::ValueToString(r, ColumnIndex(u"cue_path"_s) + col);
d->rating_ = SqlHelper::ValueToFloat(r, ColumnIndex(u"rating"_s) + col);
d->bpm_ = SqlHelper::ValueToFloat(r, ColumnIndex(u"bpm"_s) + col);
d->mood_ = SqlHelper::ValueToString(r, ColumnIndex(u"mood"_s) + col);
d->initial_key_ = SqlHelper::ValueToString(r, ColumnIndex(u"initial_key"_s) + col);
d->acoustid_id_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_id"_s) + col);
d->acoustid_fingerprint_ = SqlHelper::ValueToString(r, ColumnIndex(u"acoustid_fingerprint"_s) + col);
@@ -1871,13 +1734,9 @@ void Song::BindToQuery(SqlQuery *query) const {
// Remember to bind these in the same order as kBindSpec
query->BindStringValue(u":title"_s, d->title_);
query->BindStringValue(u":titlesort"_s, d->titlesort_);
query->BindStringValue(u":album"_s, d->album_);
query->BindStringValue(u":albumsort"_s, d->albumsort_);
query->BindStringValue(u":artist"_s, d->artist_);
query->BindStringValue(u":artistsort"_s, d->artistsort_);
query->BindStringValue(u":albumartist"_s, d->albumartist_);
query->BindStringValue(u":albumartistsort"_s, d->albumartistsort_);
query->BindIntValue(u":track"_s, d->track_);
query->BindIntValue(u":disc"_s, d->disc_);
query->BindIntValue(u":year"_s, d->year_);
@@ -1885,9 +1744,7 @@ void Song::BindToQuery(SqlQuery *query) const {
query->BindStringValue(u":genre"_s, d->genre_);
query->BindBoolValue(u":compilation"_s, d->compilation_);
query->BindStringValue(u":composer"_s, d->composer_);
query->BindStringValue(u":composersort"_s, d->composersort_);
query->BindStringValue(u":performer"_s, d->performer_);
query->BindStringValue(u":performersort"_s, d->performersort_);
query->BindStringValue(u":grouping"_s, d->grouping_);
query->BindStringValue(u":comment"_s, d->comment_);
query->BindStringValue(u":lyrics"_s, d->lyrics_);
@@ -1935,9 +1792,6 @@ void Song::BindToQuery(SqlQuery *query) const {
query->BindValue(u":cue_path"_s, d->cue_path_);
query->BindFloatValue(u":rating"_s, d->rating_);
query->BindFloatValue(u":bpm"_s, d->bpm_);
query->BindStringValue(u":mood"_s, d->mood_);
query->BindStringValue(u":initial_key"_s, d->initial_key_);
query->BindStringValue(u":acoustid_id"_s, d->acoustid_id_);
query->BindStringValue(u":acoustid_fingerprint"_s, d->acoustid_fingerprint_);
@@ -1965,7 +1819,7 @@ void Song::ToXesam(QVariantMap *map) const {
using mpris::AddMetadataAsList;
using mpris::AsMPRISDateTimeType;
AddMetadata(u"xesam:url"_s, effective_url().toString(), map);
AddMetadata(u"xesam:url"_s, effective_stream_url().toString(), map);
AddMetadata(u"xesam:title"_s, PrettyTitle(), map);
AddMetadataAsList(u"xesam:artist"_s, artist(), map);
AddMetadata(u"xesam:album"_s, album(), map);
@@ -2162,3 +2016,4 @@ QString Song::GetNameForNewPlaylist(const SongList &songs) {
return result;
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-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
@@ -105,7 +105,6 @@ class Song {
IT = 21,
SPC = 22,
VGM = 23,
ALAC = 24, // MP4, with ALAC codec
CDDA = 90,
Stream = 91
};
@@ -150,13 +149,9 @@ class Song {
bool is_valid() const;
const QString &title() const;
const QString &titlesort() const;
const QString &album() const;
const QString &albumsort() const;
const QString &artist() const;
const QString &artistsort() const;
const QString &albumartist() const;
const QString &albumartistsort() const;
int track() const;
int disc() const;
int year() const;
@@ -164,9 +159,7 @@ class Song {
const QString &genre() const;
bool compilation() const;
const QString &composer() const;
const QString &composersort() const;
const QString &performer() const;
const QString &performersort() const;
const QString &grouping() const;
const QString &comment() const;
const QString &lyrics() const;
@@ -213,9 +206,6 @@ class Song {
const QString &cue_path() const;
float rating() const;
float bpm() const;
const QString &mood() const;
const QString &initial_key() const;
const QString &acoustid_id() const;
const QString &acoustid_fingerprint() const;
@@ -234,8 +224,6 @@ class Song {
std::optional<double> ebur128_integrated_loudness_lufs() const;
std::optional<double> ebur128_loudness_range_lu() const;
int id3v2_version() const;
QString *mutable_title();
QString *mutable_album();
QString *mutable_artist();
@@ -261,6 +249,11 @@ class Song {
bool init_from_file() const;
const QString &title_sortable() const;
const QString &album_sortable() const;
const QString &artist_sortable() const;
const QString &albumartist_sortable() const;
const QUrl &stream_url() const;
// Setters
@@ -268,13 +261,9 @@ class Song {
void set_valid(const bool v);
void set_title(const QString &v);
void set_titlesort(const QString &v);
void set_album(const QString &v);
void set_albumsort(const QString &v);
void set_artist(const QString &v);
void set_artistsort(const QString &v);
void set_albumartist(const QString &v);
void set_albumartistsort(const QString &v);
void set_track(const int v);
void set_disc(const int v);
void set_year(const int v);
@@ -282,9 +271,7 @@ class Song {
void set_genre(const QString &v);
void set_compilation(bool v);
void set_composer(const QString &v);
void set_composersort(const QString &v);
void set_performer(const QString &v);
void set_performersort(const QString &v);
void set_grouping(const QString &v);
void set_comment(const QString &v);
void set_lyrics(const QString &v);
@@ -330,9 +317,6 @@ class Song {
void set_cue_path(const QString &v);
void set_rating(const float v);
void set_bpm(const float v);
void set_mood(const QString &v);
void set_initial_key(const QString &v);
void set_acoustid_id(const QString &v);
void set_acoustid_fingerprint(const QString &v);
@@ -351,25 +335,17 @@ class Song {
void set_ebur128_integrated_loudness_lufs(const std::optional<double> v);
void set_ebur128_loudness_range_lu(const std::optional<double> v);
void set_id3v2_version(const int v);
void set_init_from_file(const bool v);
void set_stream_url(const QUrl &v);
void set_title(const TagLib::String &v);
void set_titlesort(const TagLib::String &v);
void set_album(const TagLib::String &v);
void set_albumsort(const TagLib::String &v);
void set_artist(const TagLib::String &v);
void set_artistsort(const TagLib::String &v);
void set_albumartist(const TagLib::String &v);
void set_albumartistsort(const TagLib::String &v);
void set_genre(const TagLib::String &v);
void set_composer(const TagLib::String &v);
void set_composersort(const TagLib::String &v);
void set_performer(const TagLib::String &v);
void set_performersort(const TagLib::String &v);
void set_grouping(const TagLib::String &v);
void set_comment(const TagLib::String &v);
void set_lyrics(const TagLib::String &v);
@@ -388,21 +364,14 @@ class Song {
void set_musicbrainz_disc_id(const TagLib::String &v);
void set_musicbrainz_release_group_id(const TagLib::String &v);
void set_musicbrainz_work_id(const TagLib::String &v);
void set_mood(const TagLib::String &v);
void set_initial_key(const TagLib::String &v);
const QUrl &effective_url() const;
const QString &effective_titlesort() const;
const QUrl &effective_stream_url() const;
const QString &effective_albumartist() const;
const QString &effective_albumartistsort() const;
const QString &effective_artistsort() const;
const QString &effective_albumartist_sortable() const;
const QString &effective_album() const;
const QString &effective_albumsort() const;
const QString &effective_composersort() const;
const QString &effective_performersort() const;
int effective_originalyear() const;
const QString &playlist_effective_albumartist() const;
const QString &playlist_effective_albumartistsort() const;
const QString &playlist_albumartist() const;
const QString &playlist_albumartist_sortable() const;
bool is_metadata_good() const;
bool is_local_collection_song() const;
@@ -433,18 +402,9 @@ class Song {
bool comment_supported() const;
bool lyrics_supported() const;
bool albumartistsort_supported() const;
bool albumsort_supported() const;
bool artistsort_supported() const;
bool composersort_supported() const;
bool performersort_supported() const;
bool titlesort_supported() const;
static bool save_embedded_cover_supported(const FileType filetype);
bool save_embedded_cover_supported() const { return url().isLocalFile() && save_embedded_cover_supported(filetype()) && !has_cue(); };
bool id3v2_tags_supported() const;
static int ColumnIndex(const QString &field);
static QString JoinSpec(const QString &table);
@@ -470,7 +430,6 @@ class Song {
bool IsEditable() const;
// Comparison functions
bool IsFileInfoEqual(const Song &other) const;
bool IsMetadataEqual(const Song &other) const;
bool IsPlayStatisticsEqual(const Song &other) const;
bool IsRatingEqual(const Song &other) const;
@@ -479,10 +438,7 @@ class Song {
bool IsMusicBrainzEqual(const Song &other) const;
bool IsEBUR128Equal(const Song &other) const;
bool IsArtEqual(const Song &other) const;
bool IsCompilationEqual(const Song &other) const;
bool IsSettingsEqual(const Song &other) const;
bool IsAllMetadataEqual(const Song &other) const;
bool IsEqual(const Song &other) const;
bool IsOnSameAlbum(const Song &other) const;
bool IsSimilar(const Song &other) const;
@@ -492,7 +448,6 @@ class Song {
static QString DescriptionForSource(const Source source);
static Source SourceFromText(const QString &source);
static QIcon IconForSource(const Source source);
static QString DomainForSource(const Source source);
static QString TextForFiletype(const FileType filetype);
static QString ExtensionForFiletype(const FileType filetype);
static QIcon IconForFiletype(const FileType filetype);
@@ -500,12 +455,9 @@ class Song {
QString TextForSource() const { return TextForSource(source()); }
QString DescriptionForSource() const { return DescriptionForSource(source()); }
QIcon IconForSource() const { return IconForSource(source()); }
QString DomainForSource() const { return DomainForSource(source()); }
QString TextForFiletype() const { return TextForFiletype(filetype()); }
QIcon IconForFiletype() const { return IconForFiletype(filetype()); }
QString ShareURL() const;
bool IsFileLossless() const;
static FileType FiletypeByMimetype(const QString &mimetype);
static FileType FiletypeByDescription(const QString &text);
@@ -569,6 +521,9 @@ class Song {
private:
struct Private;
static QString sortable(const QString &v);
QSharedDataPointer<Private> d;
};

View File

@@ -98,9 +98,6 @@ SongLoader::SongLoader(const SharedPtr<UrlHandlers> url_handlers,
QObject::connect(timeout_timer_, &QTimer::timeout, this, &SongLoader::Timeout);
QObject::connect(playlist_parser_, &PlaylistParser::Error, this, &SongLoader::ParserError);
QObject::connect(cue_parser_, &CueParser::Error, this, &SongLoader::ParserError);
}
SongLoader::~SongLoader() {
@@ -109,10 +106,6 @@ SongLoader::~SongLoader() {
}
void SongLoader::ParserError(const QString &error) {
errors_ << error;
}
SongLoader::Result SongLoader::Load(const QUrl &url) {
if (url.isEmpty()) return Result::Error;
@@ -185,11 +178,9 @@ SongLoader::Result SongLoader::LoadLocalPartial(const QString &filename) {
SongLoader::Result SongLoader::LoadAudioCD() {
#ifdef HAVE_AUDIOCD
CDDASongLoader *cdda_song_loader = new CDDASongLoader(QUrl(), this);
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadError, this, &SongLoader::AudioCDTracksLoadErrorSlot);
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsLoaded, this, &SongLoader::AudioCDTracksLoadedSlot);
QObject::connect(cdda_song_loader, &CDDASongLoader::SongsUpdated, this, &SongLoader::AudioCDTracksUpdatedSlot);
QObject::connect(cdda_song_loader, &CDDASongLoader::LoadingFinished, this, &SongLoader::AudioCDLoadingFinishedSlot);
CddaSongLoader *cdda_song_loader = new CddaSongLoader(QUrl(), this);
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsDurationLoaded, this, &SongLoader::AudioCDTracksLoadFinishedSlot);
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsMetadataLoaded, this, &SongLoader::AudioCDTracksTagsLoaded);
cdda_song_loader->LoadSongs();
return Result::Success;
#else
@@ -201,38 +192,23 @@ SongLoader::Result SongLoader::LoadAudioCD() {
#ifdef HAVE_AUDIOCD
void SongLoader::AudioCDTracksLoadErrorSlot(const QString &error) {
void SongLoader::AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error) {
songs_ = songs;
errors_ << error;
Q_EMIT AudioCDTracksLoadFinished();
}
void SongLoader::AudioCDTracksLoadedSlot(const SongList &songs) {
void SongLoader::AudioCDTracksTagsLoaded(const SongList &songs) {
songs_ = songs;
Q_EMIT AudioCDTracksLoaded();
}
void SongLoader::AudioCDTracksUpdatedSlot(const SongList &songs) {
songs_ = songs;
Q_EMIT AudioCDTracksUpdated();
}
void SongLoader::AudioCDLoadingFinishedSlot() {
CDDASongLoader *cdda_song_loader = qobject_cast<CDDASongLoader*>(sender());
CddaSongLoader *cdda_song_loader = qobject_cast<CddaSongLoader*>(sender());
cdda_song_loader->deleteLater();
Q_EMIT AudioCDLoadingFinished(true);
songs_ = songs;
Q_EMIT LoadAudioCDFinished(true);
}
#endif // HAVE_AUDIOCD
#endif
SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
@@ -294,7 +270,6 @@ SongLoader::Result SongLoader::LoadLocalAsync(const QString &filename) {
}
if (parser) { // It's a playlist!
QObject::connect(parser, &ParserBase::Error, this, &SongLoader::ParserError, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
qLog(Debug) << "Parsing using" << parser->name();
LoadPlaylist(parser, filename);
return Result::Success;
@@ -714,10 +689,6 @@ void SongLoader::MagicReady() {
StopTypefindAsync(true);
}
if (parser_) {
QObject::connect(parser_, &ParserBase::Error, this, &SongLoader::ParserError, static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
}
state_ = State::WaitingForData;
if (!IsPipelinePlaying()) {
@@ -796,3 +767,4 @@ void SongLoader::CleanupPipeline() {
state_ = State::Finished;
}

View File

@@ -50,7 +50,7 @@ class ParserBase;
class CueParser;
#ifdef HAVE_AUDIOCD
class CDDASongLoader;
class CddaSongLoader;
#endif
class SongLoader : public QObject {
@@ -90,22 +90,17 @@ class SongLoader : public QObject {
QStringList errors() { return errors_; }
Q_SIGNALS:
void AudioCDTracksLoaded();
void AudioCDTracksUpdated();
void AudioCDLoadingFinished(const bool success);
void AudioCDTracksLoadFinished();
void LoadAudioCDFinished(const bool success);
void LoadRemoteFinished();
private Q_SLOTS:
void ScheduleTimeout();
void Timeout();
void StopTypefind();
void ParserError(const QString &error);
#ifdef HAVE_AUDIOCD
void AudioCDTracksLoadErrorSlot(const QString &error);
void AudioCDTracksLoadedSlot(const SongList &songs);
void AudioCDTracksUpdatedSlot(const SongList &songs);
void AudioCDLoadingFinishedSlot();
void AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error);
void AudioCDTracksTagsLoaded(const SongList &songs);
#endif // HAVE_AUDIOCD
private:
@@ -172,6 +167,7 @@ class SongLoader : public QObject {
QStringList errors_;
bool success_;
};
#endif // SONGLOADER_H

View File

@@ -41,3 +41,4 @@ class SongMimeData : public MimeData {
};
#endif // SONGMIMEDATA_H

View File

@@ -31,8 +31,8 @@
#include <QTextStream>
#include <QFile>
#include <QString>
#include <QPalette>
#include <QColor>
#include <QPalette>
#include <QEvent>
#include "includes/shared_ptr.h"
@@ -80,13 +80,20 @@ void StyleSheetLoader::UpdateStyleSheet(QWidget *widget, SharedPtr<StyleSheetDat
// Replace %palette-role with actual colours
QPalette p(widget->palette());
QColor color_altbase = p.color(QPalette::AlternateBase);
{
QColor alt = p.color(QPalette::AlternateBase);
#ifdef Q_OS_MACOS
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? (color_altbase.lightness() > 180 ? 130 : 16) : color_altbase.alpha());
if (alt.lightness() > 180) {
alt.setAlpha(130);
}
else {
alt.setAlpha(16);
}
#else
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? 116 : color_altbase.alpha());
alt.setAlpha(130);
#endif
stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(color_altbase.red()).arg(color_altbase.green()).arg(color_altbase.blue()).arg(color_altbase.alpha()));
stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(alt.red()).arg(alt.green()).arg(alt.blue()).arg(alt.alpha()));
}
ReplaceColor(&stylesheet, u"Window"_s, p, QPalette::Window);
ReplaceColor(&stylesheet, u"Background"_s, p, QPalette::Window);

View File

@@ -41,19 +41,17 @@ Translations::~Translations() {
}
bool Translations::LoadTranslation(const QString &prefix, const QString &path, const QString &language) {
void Translations::LoadTranslation(const QString &prefix, const QString &path, const QString &language) {
const QString basefilename = prefix + u'_' + language;
QTranslator *t = new QTranslator;
if (!t->load(basefilename, path)) {
delete t;
return false;
}
if (t->load(basefilename, path)) {
qLog(Debug) << "Tranlations loaded from" << basefilename;
QCoreApplication::installTranslator(t);
translations_ << t;
return true;
}
else {
delete t;
}
}

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