Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c658a77b05 | ||
|
|
1880dc8153 | ||
|
|
b5fd3d5717 | ||
|
|
3c3480fb84 | ||
|
|
f628914173 | ||
|
|
c100fb1bb8 | ||
|
|
8c804c4fba | ||
|
|
912a7c7da9 | ||
|
|
0a5815c82e | ||
|
|
6513b3032b | ||
|
|
8c51401bdc | ||
|
|
45fc9c83d4 | ||
|
|
be57d8147a | ||
|
|
a97908fb6b | ||
|
|
c0417d4bb3 | ||
|
|
062e2cfb84 | ||
|
|
700f7dbe36 | ||
|
|
0487118dad | ||
|
|
f3d088e48b | ||
|
|
f8afd49fcf | ||
|
|
363fcb5aba | ||
|
|
183aba4181 | ||
|
|
742be01aa6 | ||
|
|
38c8054873 | ||
|
|
da9e9840b8 | ||
|
|
c4646531b0 | ||
|
|
65d9b6a9e9 | ||
|
|
046f40fbca | ||
|
|
a0ca50ac30 | ||
|
|
d939733675 | ||
|
|
61a8a3a84a | ||
|
|
d4858a338c | ||
|
|
31380a5bd4 | ||
|
|
e45b9aabeb | ||
|
|
27e782d8cf | ||
|
|
0bfc2ee198 | ||
|
|
e7fc4b1706 | ||
|
|
6dea1a2149 | ||
|
|
7844a2b932 | ||
|
|
96a53bfbe5 | ||
|
|
fe5fbae4b4 | ||
|
|
a9140232e5 | ||
|
|
835090dd96 | ||
|
|
af5590dcb1 | ||
|
|
26b5588d7d | ||
|
|
390bf049f2 | ||
|
|
321272b695 | ||
|
|
342805e0f3 | ||
|
|
e614626913 | ||
|
|
2ddacf2f98 | ||
|
|
a47531d4ce | ||
|
|
84b758e395 | ||
|
|
51b69a85c4 | ||
|
|
52774a3222 | ||
|
|
9030b2567b | ||
|
|
ee7bb449a5 | ||
|
|
d901258f11 | ||
|
|
6372c5ee7d | ||
|
|
75f0402793 | ||
|
|
20e5c014ef | ||
|
|
1ebc32c3aa | ||
|
|
a5f94b608b | ||
|
|
e0d61223a4 | ||
|
|
459eea5bc4 | ||
|
|
09d02c53a3 | ||
|
|
61a701554e | ||
|
|
d280e6426f | ||
|
|
5b9bb3efa7 | ||
|
|
b8cbe49f8c | ||
|
|
633e5707ef | ||
|
|
d54290c3a7 | ||
|
|
3ef2b53e46 | ||
|
|
d3a4dd6da6 | ||
|
|
0158f7f08a | ||
|
|
8cea020fac | ||
|
|
f6b38fecb0 | ||
|
|
5e2729fafe | ||
|
|
19dce1c25d | ||
|
|
00bb722e25 | ||
|
|
cbaf4d3121 | ||
|
|
4b5370044b | ||
|
|
ffbe1ec9fd | ||
|
|
53e43db91b | ||
|
|
2858cdabc2 | ||
|
|
cf74eeb120 | ||
|
|
790a1b4dbf | ||
|
|
ee6332af1e | ||
|
|
bf0704f6b2 | ||
|
|
ae13fe7f52 | ||
|
|
90678e72ac | ||
|
|
a0ec244008 | ||
|
|
fba4f84fb6 | ||
|
|
950774f1c8 | ||
|
|
340bc21537 | ||
|
|
a86ba4dffc | ||
|
|
d6bc6e33c0 | ||
|
|
7e128a9af5 | ||
|
|
0f0746be9d | ||
|
|
bec3fe9fd5 | ||
|
|
83c666baf9 | ||
|
|
b9b54e6e96 | ||
|
|
b2ff6240eb | ||
|
|
26a7c74a24 | ||
|
|
a34954ec4a | ||
|
|
349ab62e75 | ||
|
|
65e960f2c5 | ||
|
|
e22fef8ca4 | ||
|
|
3e99045e2c | ||
|
|
4fcade273e | ||
|
|
5eaff0d26e | ||
|
|
5b22f12b4a | ||
|
|
5f85c2e7a5 | ||
|
|
4fb5a7b6bc | ||
|
|
04c6c862c4 | ||
|
|
baec45f742 | ||
|
|
9efdbd2c10 | ||
|
|
d8800b80d5 | ||
|
|
ec715abb0d | ||
|
|
1485801efb | ||
|
|
4f9ac3d33a | ||
|
|
1577ce4d67 | ||
|
|
7eee74a2e9 | ||
|
|
d9e38fb3be | ||
|
|
81cc90e54a | ||
|
|
bd9771a88f | ||
|
|
f5cd81fe09 | ||
|
|
277e2cff59 | ||
|
|
6fa9514059 | ||
|
|
c5e38b71f7 | ||
|
|
3746915ae7 | ||
|
|
21bdf88d09 | ||
|
|
ff032c3cd7 | ||
|
|
c083110051 | ||
|
|
a7dbeb5d76 | ||
|
|
634f6ea9f5 | ||
|
|
f9e4f9a09a | ||
|
|
aab9889174 |
128
.github/workflows/build.yml
vendored
128
.github/workflows/build.yml
vendored
@@ -97,7 +97,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
fedora_version: [ '39', '40', '41', '42' ]
|
||||
fedora_version: [ '41', '42', '43' ]
|
||||
container:
|
||||
image: fedora:${{matrix.fedora_version}}
|
||||
steps:
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
- name: Remove files
|
||||
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -409,7 +409,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -507,7 +507,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -538,7 +538,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
|
||||
ubuntu_version: [ 'noble', 'plucky' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -599,7 +599,7 @@ jobs:
|
||||
cmake --build build --config Release --parallel 4
|
||||
cmake --install build
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -631,7 +631,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ubuntu_version: [ 'noble', 'oracular', 'plucky' ]
|
||||
ubuntu_version: [ 'noble', 'plucky' ]
|
||||
container:
|
||||
image: ubuntu:${{matrix.ubuntu_version}}
|
||||
steps:
|
||||
@@ -691,7 +691,7 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: apt install -y keyboxd
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -727,13 +727,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Build FreeBSD
|
||||
id: build-freebsd
|
||||
uses: vmactions/freebsd-vm@v1.2.0
|
||||
uses: vmactions/freebsd-vm@v1.2.3
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
@@ -752,13 +752,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: Build OpenBSD
|
||||
id: build-openbsd
|
||||
uses: vmactions/openbsd-vm@v1.1.7
|
||||
uses: vmactions/openbsd-vm@v1.2.0
|
||||
with:
|
||||
usesh: true
|
||||
mem: 4096
|
||||
@@ -788,7 +788,7 @@ jobs:
|
||||
|
||||
- name: Set MACOSX_DEPLOYMENT_TARGET
|
||||
run: |
|
||||
for i in 13 14 15; do
|
||||
for i in 12 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
|
||||
@@ -818,7 +818,7 @@ jobs:
|
||||
rm -f uninstall.sh
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -946,7 +946,7 @@ jobs:
|
||||
|
||||
- name: Set MACOSX_DEPLOYMENT_TARGET
|
||||
run: |
|
||||
for i in 13 14 15; do
|
||||
for i in 12 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
|
||||
@@ -969,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@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1072,7 +1072,7 @@ jobs:
|
||||
run: echo "cmake_buildtype=$(echo ${{matrix.buildtype}} | sed 's/.*/\u&/')" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1246,12 +1246,42 @@ 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:
|
||||
arch: [ 'x86', 'x86_64' ]
|
||||
buildtype: [ 'release' ]
|
||||
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}}
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set prefix path
|
||||
@@ -1265,6 +1295,20 @@ 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.19041.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
|
||||
@@ -1347,11 +1391,11 @@ jobs:
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: ${{matrix.arch}}
|
||||
sdk: 10.0.20348.0
|
||||
sdk: ${{env.sdk_version}}
|
||||
vsversion: 2022
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -1364,15 +1408,18 @@ jobs:
|
||||
shell: cmd
|
||||
run: cmake -E make_directory build
|
||||
|
||||
- name: Set ENABLE_WIN32_CONSOLE (debug)
|
||||
if: matrix.buildtype == 'debug'
|
||||
- name: Set ENABLE_WIN32_CONSOLE
|
||||
shell: bash
|
||||
run: echo "win32_console=ON" >> $GITHUB_ENV
|
||||
run: echo "enable_win32_console=$(test "${{matrix.buildtype}}" = "debug" && echo "ON" || echo "OFF")" >> $GITHUB_ENV
|
||||
|
||||
- name: Set ENABLE_WIN32_CONSOLE (release)
|
||||
if: matrix.buildtype == 'release'
|
||||
- name: Set ENABLE_SPOTIFY
|
||||
shell: bash
|
||||
run: echo "win32_console=OFF" >> $GITHUB_ENV
|
||||
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
|
||||
|
||||
- name: Run CMake
|
||||
shell: cmd
|
||||
@@ -1384,14 +1431,14 @@ jobs:
|
||||
-DCMAKE_BUILD_TYPE="${{env.cmake_buildtype}}"
|
||||
-DCMAKE_PREFIX_PATH="${{env.prefix_path_forwardslash}}/lib/cmake"
|
||||
-DARCH="${{matrix.arch}}"
|
||||
-DENABLE_WIN32_CONSOLE=${{env.win32_console}}
|
||||
-DENABLE_WIN32_CONSOLE=${{env.enable_win32_console}}
|
||||
-DPKG_CONFIG_EXECUTABLE="${{env.prefix_path_forwardslash}}/bin/pkg-config.exe"
|
||||
-DICU_ROOT="${{env.prefix_path_forwardslash}}"
|
||||
-DENABLE_GIO=OFF
|
||||
-DENABLE_AUDIOCD=OFF
|
||||
-DENABLE_MTP=OFF
|
||||
-DENABLE_GPOD=OFF
|
||||
-DENABLE_SPOTIFY=ON
|
||||
-DENABLE_SPOTIFY=${{env.enable_spotify}}
|
||||
|
||||
- name: Run Make
|
||||
shell: cmd
|
||||
@@ -1460,11 +1507,14 @@ jobs:
|
||||
run: copy ${{env.prefix_path_backslash}}\lib\gstreamer-1.0\*.dll .\gstreamer-plugins\
|
||||
|
||||
- name: Download copydlldeps.sh
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: curl -f -O -L https://raw.githubusercontent.com/strawberrymusicplayer/strawberry-mxe/master/tools/copydlldeps.sh
|
||||
|
||||
- name: Copy dependencies
|
||||
# copydlldeps.sh doesn't work with arm64 binaries.
|
||||
if: matrix.arch != 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: >
|
||||
@@ -1481,6 +1531,12 @@ jobs:
|
||||
-F ./gstreamer-plugins
|
||||
-R ${{env.prefix_path_unix}}/bin
|
||||
|
||||
- name: Copy dependencies
|
||||
if: matrix.arch == 'arm64'
|
||||
shell: bash
|
||||
working-directory: build
|
||||
run: cp -v ${{env.prefix_path_unix}}/bin/{avcodec*.dll,avfilter*.dll,avformat*.dll,avutil*.dll,brotlicommon.dll,brotlidec.dll,chromaprint.dll,ebur128.dll,faad-2.dll,fdk-aac.dll,ffi-7.dll,FLAC.dll,freetype*.dll,getopt.dll,gio-2.0-0.dll,glib-2.0-0.dll,gme.dll,gmodule-2.0-0.dll,gobject-2.0-0.dll,gst-discoverer-1.0.exe,gst-launch-1.0.exe,gst-play-1.0.exe,gstadaptivedemux-1.0-0.dll,gstapp-1.0-0.dll,gstaudio-1.0-0.dll,gstbadaudio-1.0-0.dll,gstbase-1.0-0.dll,gstcodecparsers-1.0-0.dll,gstfft-1.0-0.dll,gstisoff-1.0-0.dll,gstmpegts-1.0-0.dll,gstnet-1.0-0.dll,gstpbutils-1.0-0.dll,gstreamer-1.0-0.dll,gstriff-1.0-0.dll,gstrtp-1.0-0.dll,gstrtsp-1.0-0.dll,gstsdp-1.0-0.dll,gsttag-1.0-0.dll,gsturidownloader-1.0-0.dll,gstvideo-1.0-0.dll,gstwinrt-1.0-0.dll,harfbuzz.dll,icudt*.dll,icuin*.dll,icuuc*.dll,intl-8.dll,jpeg62.dll,kdsingleapplication*.dll,libbs2b.dll,libcrypto-3-*.dll,fftw3.dll,libiconv*.dll,liblzma.dll,libmp3lame.dll,libopenmpt.dll,libpng16*.dll,libspeex*.dll,libssl-3-*.dll,libxml2*.dll,mpcdec.dll,mpg123.dll,nghttp2.dll,ogg.dll,opus.dll,orc-0.4-0.dll,pcre2-16*.dll,pcre2-8*.dll,postproc*.dll,psl-5.dll,Qt6Concurrent*.dll,Qt6Core*.dll,Qt6Gui*.dll,Qt6Network*.dll,Qt6Sql*.dll,Qt6Widgets*.dll,qtsparkle-qt6.dll,soup-3.0-0.dll,sqlite3.dll,sqlite3.exe,swresample*.dll,swscale*.dll,tag.dll,vorbis.dll,vorbisfile.dll,wavpackdll.dll,zlib*.dll} .
|
||||
|
||||
- name: Copy nsis files
|
||||
shell: cmd
|
||||
working-directory: build
|
||||
@@ -1581,11 +1637,11 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: sudo apt install -y git rsync
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: artifacts
|
||||
- name: SSH Setup
|
||||
@@ -1629,7 +1685,7 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: sudo apt install -y git jq gh
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Show release assets
|
||||
@@ -1637,7 +1693,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@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Add artifacts to release
|
||||
|
||||
42
3rdparty/discord-rpc/CMakeLists.txt
vendored
42
3rdparty/discord-rpc/CMakeLists.txt
vendored
@@ -1 +1,41 @@
|
||||
add_subdirectory(src)
|
||||
set(DISCORD_RPC_SOURCES
|
||||
discord_rpc.h
|
||||
discord_register.h
|
||||
discord_rpc.cpp
|
||||
discord_rpc_connection.h
|
||||
discord_rpc_connection.cpp
|
||||
discord_serialization.h
|
||||
discord_serialization.cpp
|
||||
discord_connection.h
|
||||
discord_backoff.h
|
||||
discord_msg_queue.h
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_connection_unix.cpp)
|
||||
if(APPLE)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
|
||||
add_definitions(-DDISCORD_OSX)
|
||||
else()
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
|
||||
add_definitions(-DDISCORD_LINUX)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_connection_win.cpp discord_register_win.cpp)
|
||||
add_definitions(-DDISCORD_WINDOWS)
|
||||
endif()
|
||||
|
||||
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||
endif()
|
||||
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
63
3rdparty/discord-rpc/discord_backoff.h
vendored
Normal file
63
3rdparty/discord-rpc/discord_backoff.h
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_BACKOFF_H
|
||||
#define DISCORD_BACKOFF_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
struct Backoff {
|
||||
int64_t minAmount;
|
||||
int64_t maxAmount;
|
||||
int64_t current;
|
||||
int fails;
|
||||
std::mt19937_64 randGenerator;
|
||||
std::uniform_real_distribution<> randDistribution;
|
||||
|
||||
double rand01() { return randDistribution(randGenerator); }
|
||||
|
||||
Backoff(int64_t min, int64_t max)
|
||||
: minAmount(min), maxAmount(max), current(min), fails(0), randGenerator(static_cast<uint64_t>(time(0))) {
|
||||
}
|
||||
|
||||
void reset() {
|
||||
fails = 0;
|
||||
current = minAmount;
|
||||
}
|
||||
|
||||
int64_t nextDelay() {
|
||||
++fails;
|
||||
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
|
||||
current = std::min(current + delay, maxAmount);
|
||||
return current;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_BACKOFF_H
|
||||
48
3rdparty/discord-rpc/discord_connection.h
vendored
Normal file
48
3rdparty/discord-rpc/discord_connection.h
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_CONNECTION_H
|
||||
#define DISCORD_CONNECTION_H
|
||||
|
||||
// This is to wrap the platform specific kinds of connect/read/write.
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// not really connectiony, but need per-platform
|
||||
int GetProcessId();
|
||||
|
||||
struct BaseConnection {
|
||||
static BaseConnection *Create();
|
||||
static void Destroy(BaseConnection *&);
|
||||
bool isOpen = false;
|
||||
bool Open();
|
||||
bool Close();
|
||||
bool Write(const void *data, size_t length);
|
||||
bool Read(void *data, size_t length);
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_CONNECTION_H
|
||||
@@ -1,4 +1,27 @@
|
||||
#include "connection.h"
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_connection.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
@@ -28,28 +51,34 @@ static int MsgFlags = 0;
|
||||
#endif
|
||||
|
||||
static const char *GetTempPath() {
|
||||
|
||||
const char *temp = getenv("XDG_RUNTIME_DIR");
|
||||
temp = temp ? temp : getenv("TMPDIR");
|
||||
temp = temp ? temp : getenv("TMP");
|
||||
temp = temp ? temp : getenv("TEMP");
|
||||
temp = temp ? temp : "/tmp";
|
||||
|
||||
return temp;
|
||||
|
||||
}
|
||||
|
||||
/*static*/ BaseConnection *BaseConnection::Create() {
|
||||
BaseConnection *BaseConnection::Create() {
|
||||
PipeAddr.sun_family = AF_UNIX;
|
||||
return &Connection;
|
||||
}
|
||||
|
||||
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(c);
|
||||
void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
|
||||
self->Close();
|
||||
c = nullptr;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Open() {
|
||||
|
||||
const char *tempPath = GetTempPath();
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
@@ -61,8 +90,7 @@ bool BaseConnection::Open() {
|
||||
#endif
|
||||
|
||||
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
|
||||
snprintf(
|
||||
PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
|
||||
snprintf(PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
|
||||
int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr));
|
||||
if (err == 0) {
|
||||
self->isOpen = true;
|
||||
@@ -70,10 +98,13 @@ bool BaseConnection::Open() {
|
||||
}
|
||||
}
|
||||
self->Close();
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
@@ -81,11 +112,14 @@ bool BaseConnection::Close() {
|
||||
close(self->sock);
|
||||
self->sock = -1;
|
||||
self->isOpen = false;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Write(const void *data, size_t length) {
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
@@ -95,11 +129,14 @@ bool BaseConnection::Write(const void *data, size_t length) {
|
||||
if (sentBytes < 0) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return sentBytes == static_cast<ssize_t>(length);
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Read(void *data, size_t length) {
|
||||
auto self = reinterpret_cast<BaseConnectionUnix *>(this);
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
||||
|
||||
if (self->sock == -1) {
|
||||
return false;
|
||||
@@ -115,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
|
||||
else if (res == 0) {
|
||||
Close();
|
||||
}
|
||||
|
||||
return static_cast<size_t>(res) == length;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
} // namespace discord_rpc
|
||||
@@ -1,9 +1,33 @@
|
||||
#include "connection.h"
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_connection.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
|
||||
#include <cassert>
|
||||
#include <windows.h>
|
||||
|
||||
@@ -19,24 +43,26 @@ struct BaseConnectionWin : public BaseConnection {
|
||||
|
||||
static BaseConnectionWin Connection;
|
||||
|
||||
/*static*/ BaseConnection *BaseConnection::Create() {
|
||||
BaseConnection *BaseConnection::Create() {
|
||||
return &Connection;
|
||||
}
|
||||
|
||||
/*static*/ void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
void BaseConnection::Destroy(BaseConnection *&c) {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionWin*>(c);
|
||||
self->Close();
|
||||
c = nullptr;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Open() {
|
||||
|
||||
wchar_t pipeName[] { L"\\\\?\\pipe\\discord-ipc-0" };
|
||||
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
||||
pipeName[pipeDigit] = L'0';
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
for (;;) {
|
||||
self->pipe = ::CreateFileW(
|
||||
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (self->pipe != INVALID_HANDLE_VALUE) {
|
||||
self->isOpen = true;
|
||||
return true;
|
||||
@@ -57,17 +83,22 @@ bool BaseConnection::Open() {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Close() {
|
||||
|
||||
auto self = reinterpret_cast<BaseConnectionWin *>(this);
|
||||
::CloseHandle(self->pipe);
|
||||
self->pipe = INVALID_HANDLE_VALUE;
|
||||
self->isOpen = false;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Write(const void *data, size_t length) {
|
||||
|
||||
if (length == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -85,11 +116,13 @@ bool BaseConnection::Write(const void *data, size_t length) {
|
||||
}
|
||||
const DWORD bytesLength = static_cast<DWORD>(length);
|
||||
DWORD bytesWritten = 0;
|
||||
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE &&
|
||||
bytesWritten == bytesLength;
|
||||
|
||||
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE && bytesWritten == bytesLength;
|
||||
|
||||
}
|
||||
|
||||
bool BaseConnection::Read(void *data, size_t length) {
|
||||
|
||||
assert(data);
|
||||
if (!data) {
|
||||
return false;
|
||||
@@ -119,8 +152,9 @@ bool BaseConnection::Read(void *data, size_t length) {
|
||||
else {
|
||||
Close();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
64
3rdparty/discord-rpc/discord_msg_queue.h
vendored
Normal file
64
3rdparty/discord-rpc/discord_msg_queue.h
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_MSG_QUEUE_H
|
||||
#define DISCORD_MSG_QUEUE_H
|
||||
|
||||
#include <atomic>
|
||||
|
||||
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
|
||||
// a consumer. Mutex up as needed.
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
template<typename ElementType, std::size_t QueueSize>
|
||||
class MsgQueue {
|
||||
ElementType queue_[QueueSize];
|
||||
std::atomic_uint nextAdd_ { 0 };
|
||||
std::atomic_uint nextSend_ { 0 };
|
||||
std::atomic_uint pendingSends_ { 0 };
|
||||
|
||||
public:
|
||||
MsgQueue() {}
|
||||
|
||||
ElementType *GetNextAddMessage() {
|
||||
// if we are falling behind, bail
|
||||
if (pendingSends_.load() >= QueueSize) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = (nextAdd_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitAdd() { ++pendingSends_; }
|
||||
|
||||
bool HavePendingSends() const { return pendingSends_.load() != 0; }
|
||||
ElementType *GetNextSendMessage() {
|
||||
auto index = (nextSend_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitSend() { --pendingSends_; }
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_MSG_QUEUE_H
|
||||
37
3rdparty/discord-rpc/discord_register.h
vendored
Normal file
37
3rdparty/discord-rpc/discord_register.h
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_REGISTER_H
|
||||
#define DISCORD_REGISTER_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void Discord_Register(const char *applicationId, const char *command);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // DISCORD_REGISTER_H
|
||||
120
3rdparty/discord-rpc/discord_register_linux.cpp
vendored
Normal file
120
3rdparty/discord-rpc/discord_register_linux.cpp
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_register.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <errno.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
static bool Mkdir(const char *path) {
|
||||
int result = mkdir(path, 0755);
|
||||
if (result == 0) {
|
||||
return true;
|
||||
}
|
||||
if (errno == EEXIST) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // 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) {
|
||||
|
||||
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
|
||||
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
char exePath[1024]{};
|
||||
if (!command || !command[0]) {
|
||||
const ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
|
||||
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
|
||||
return;
|
||||
}
|
||||
exePath[size] = '\0';
|
||||
command = exePath;
|
||||
}
|
||||
|
||||
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
|
||||
"Name=Game %s\n"
|
||||
"Exec=%s %%u\n" // note: it really wants that %u in there
|
||||
"Type=Application\n"
|
||||
"NoDisplay=true\n"
|
||||
"Categories=Discord;Games;\n"
|
||||
"MimeType=x-scheme-handler/discord-%s;\n";
|
||||
char desktopFile[2048]{};
|
||||
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
|
||||
if (fileLen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char desktopFilename[256]{};
|
||||
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
|
||||
|
||||
char desktopFilePath[1024]{};
|
||||
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/share");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/applications");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, desktopFilename);
|
||||
|
||||
FILE *fp = fopen(desktopFilePath, "w");
|
||||
if (fp) {
|
||||
fwrite(desktopFile, 1, fileLen, fp);
|
||||
fclose(fp);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
char xdgMimeCommand[1024]{};
|
||||
snprintf(xdgMimeCommand,
|
||||
sizeof(xdgMimeCommand),
|
||||
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
|
||||
applicationId,
|
||||
applicationId);
|
||||
if (system(xdgMimeCommand) < 0) {
|
||||
fprintf(stderr, "Failed to register mime handler\n");
|
||||
}
|
||||
|
||||
}
|
||||
99
3rdparty/discord-rpc/discord_register_osx.m
vendored
Normal file
99
3rdparty/discord-rpc/discord_register_osx.m
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#include "discord_register.h"
|
||||
|
||||
static void RegisterCommand(const char *applicationId, const char *command) {
|
||||
|
||||
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
|
||||
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
|
||||
// the command therein (will pass to js's window.open, so requires a url-like thing)
|
||||
|
||||
// Note: will not work for sandboxed apps
|
||||
NSString *home = NSHomeDirectory();
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
|
||||
stringByAppendingPathComponent:@"Application Support"]
|
||||
stringByAppendingPathComponent:@"discord"]
|
||||
stringByAppendingPathComponent:@"games"]
|
||||
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
|
||||
stringByAppendingPathExtension:@"json"];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
|
||||
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
|
||||
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
|
||||
|
||||
}
|
||||
|
||||
static void RegisterURL(const char *applicationId) {
|
||||
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "discord-%s", applicationId);
|
||||
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
|
||||
|
||||
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
|
||||
if (!myBundleId) {
|
||||
fprintf(stderr, "No bundle id found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
|
||||
if (!myURL) {
|
||||
fprintf(stderr, "No bundle url found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Discord_Register(const char *applicationId, const char *command) {
|
||||
|
||||
if (command) {
|
||||
RegisterCommand(applicationId, command);
|
||||
}
|
||||
else {
|
||||
// raii lite
|
||||
@autoreleasepool {
|
||||
RegisterURL(applicationId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,26 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_register.h"
|
||||
|
||||
@@ -5,6 +28,7 @@
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#include <cstdio>
|
||||
@@ -46,12 +70,8 @@ static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat,
|
||||
#endif
|
||||
#define RegSetKeyValueW regset
|
||||
|
||||
static LSTATUS regset(HKEY hkey,
|
||||
LPCWSTR subkey,
|
||||
LPCWSTR name,
|
||||
DWORD type,
|
||||
const void *data,
|
||||
DWORD len) {
|
||||
static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) {
|
||||
|
||||
HKEY htkey = hkey, hsubkey = nullptr;
|
||||
LSTATUS ret;
|
||||
if (subkey && subkey[0]) {
|
||||
@@ -64,16 +84,18 @@ static LSTATUS regset(HKEY hkey,
|
||||
if (hsubkey && hsubkey != hkey)
|
||||
RegCloseKey(hsubkey);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
|
||||
// we want to register games so we can run them as discord-<appid>://
|
||||
// Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions.
|
||||
|
||||
wchar_t exeFilePath[MAX_PATH];
|
||||
wchar_t exeFilePath[MAX_PATH]{};
|
||||
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
|
||||
wchar_t openCommand[1024];
|
||||
wchar_t openCommand[1024]{};
|
||||
|
||||
if (command && command[0]) {
|
||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
|
||||
@@ -83,18 +105,16 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
|
||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
|
||||
}
|
||||
|
||||
wchar_t protocolName[64];
|
||||
wchar_t protocolName[64]{};
|
||||
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
|
||||
wchar_t protocolDescription[128];
|
||||
StringCbPrintfW(
|
||||
protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
|
||||
wchar_t protocolDescription[128]{};
|
||||
StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
|
||||
wchar_t urlProtocol = 0;
|
||||
|
||||
wchar_t keyName[256];
|
||||
wchar_t keyName[256]{};
|
||||
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
|
||||
HKEY key;
|
||||
auto status =
|
||||
RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
|
||||
auto status = RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fprintf(stderr, "Error creating key\n");
|
||||
return;
|
||||
@@ -102,8 +122,7 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
|
||||
DWORD len;
|
||||
LSTATUS result;
|
||||
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
|
||||
result =
|
||||
RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
|
||||
result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing description\n");
|
||||
}
|
||||
@@ -114,26 +133,26 @@ static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *comma
|
||||
fprintf(stderr, "Error writing description\n");
|
||||
}
|
||||
|
||||
result = RegSetKeyValueW(
|
||||
key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
|
||||
result = RegSetKeyValueW(key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing icon\n");
|
||||
}
|
||||
|
||||
len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
|
||||
result = RegSetKeyValueW(
|
||||
key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
|
||||
result = RegSetKeyValueW(key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
|
||||
if (FAILED(result)) {
|
||||
fprintf(stderr, "Error writing command\n");
|
||||
}
|
||||
RegCloseKey(key);
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_Register(const char *applicationId, const char *command) {
|
||||
wchar_t appId[32];
|
||||
|
||||
wchar_t appId[32]{};
|
||||
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
|
||||
|
||||
wchar_t openCommand[1024];
|
||||
wchar_t openCommand[1024]{};
|
||||
const wchar_t *wcommand = nullptr;
|
||||
if (command && command[0]) {
|
||||
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
|
||||
@@ -142,42 +161,6 @@ extern "C" void Discord_Register(const char *applicationId, const char *command)
|
||||
}
|
||||
|
||||
Discord_RegisterW(appId, wcommand);
|
||||
}
|
||||
|
||||
extern "C" void Discord_RegisterSteamGame(const char *applicationId,
|
||||
const char *steamId) {
|
||||
wchar_t appId[32];
|
||||
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
|
||||
|
||||
wchar_t wSteamId[32];
|
||||
MultiByteToWideChar(CP_UTF8, 0, steamId, -1, wSteamId, 32);
|
||||
|
||||
HKEY key;
|
||||
auto status = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Valve\\Steam", 0, KEY_READ, &key);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fprintf(stderr, "Error opening Steam key\n");
|
||||
return;
|
||||
}
|
||||
|
||||
wchar_t steamPath[MAX_PATH];
|
||||
DWORD pathBytes = sizeof(steamPath);
|
||||
status = RegQueryValueExW(key, L"SteamExe", nullptr, nullptr, (BYTE *)steamPath, &pathBytes);
|
||||
RegCloseKey(key);
|
||||
if (status != ERROR_SUCCESS || pathBytes < 1) {
|
||||
fprintf(stderr, "Error reading SteamExe key\n");
|
||||
return;
|
||||
}
|
||||
|
||||
DWORD pathChars = pathBytes / sizeof(wchar_t);
|
||||
for (DWORD i = 0; i < pathChars; ++i) {
|
||||
if (steamPath[i] == L'/') {
|
||||
steamPath[i] = L'\\';
|
||||
}
|
||||
}
|
||||
|
||||
wchar_t command[1024];
|
||||
StringCbPrintfW(command, sizeof(command), L"\"%s\" steam://rungameid/%s", steamPath, wSteamId);
|
||||
|
||||
Discord_RegisterW(appId, command);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,44 @@
|
||||
#include "discord_rpc.h"
|
||||
|
||||
#include "backoff.h"
|
||||
#include "discord_register.h"
|
||||
#include "msg_queue.h"
|
||||
#include "rpc_connection.h"
|
||||
#include "serialization.h"
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
|
||||
namespace discord_rpc {
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_backoff.h"
|
||||
#include "discord_register.h"
|
||||
#include "discord_msg_queue.h"
|
||||
#include "discord_rpc_connection.h"
|
||||
#include "discord_serialization.h"
|
||||
|
||||
using namespace discord_rpc;
|
||||
|
||||
static void Discord_UpdateConnection();
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MaxMessageSize { 16 * 1024 };
|
||||
constexpr size_t MessageQueueSize { 8 };
|
||||
@@ -67,14 +92,12 @@ static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
||||
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
||||
static User connectedUser;
|
||||
|
||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential
|
||||
// backoff from 0.5 seconds to 1 minute
|
||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
|
||||
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
||||
static auto NextConnect = std::chrono::system_clock::now();
|
||||
static int Pid { 0 };
|
||||
static int Nonce { 1 };
|
||||
|
||||
static void Discord_UpdateConnection(void);
|
||||
class IoThreadHolder {
|
||||
private:
|
||||
std::atomic_bool keepRunning { true };
|
||||
@@ -108,14 +131,55 @@ class IoThreadHolder {
|
||||
|
||||
~IoThreadHolder() { Stop(); }
|
||||
};
|
||||
|
||||
static IoThreadHolder *IoThread { nullptr };
|
||||
|
||||
static void UpdateReconnectTime() {
|
||||
NextConnect = std::chrono::system_clock::now() +
|
||||
std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
|
||||
|
||||
NextConnect = std::chrono::system_clock::now() + std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
|
||||
|
||||
}
|
||||
|
||||
static void Discord_UpdateConnection(void) {
|
||||
static void SignalIOActivity() {
|
||||
|
||||
if (IoThread != nullptr) {
|
||||
IoThread->Notify();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static bool RegisterForEvent(const char *evtName) {
|
||||
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length = JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
static bool DeregisterForEvent(const char *evtName) {
|
||||
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length = JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void Discord_UpdateConnection() {
|
||||
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
@@ -217,54 +281,18 @@ static void Discord_UpdateConnection(void) {
|
||||
SendQueue.CommitSend();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void SignalIOActivity() {
|
||||
if (IoThread != nullptr) {
|
||||
IoThread->Notify();
|
||||
}
|
||||
}
|
||||
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
|
||||
|
||||
static bool RegisterForEvent(const char *evtName) {
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length =
|
||||
JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool DeregisterForEvent(const char *evtName) {
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length =
|
||||
JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" void Discord_Initialize(const char *applicationId,
|
||||
DiscordEventHandlers *handlers,
|
||||
int autoRegister,
|
||||
const char *optionalSteamId) {
|
||||
IoThread = new (std::nothrow) IoThreadHolder();
|
||||
if (IoThread == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoRegister) {
|
||||
if (optionalSteamId && optionalSteamId[0]) {
|
||||
Discord_RegisterSteamGame(applicationId, optionalSteamId);
|
||||
}
|
||||
else {
|
||||
Discord_Register(applicationId, nullptr);
|
||||
}
|
||||
Discord_Register(applicationId, nullptr);
|
||||
}
|
||||
|
||||
Pid = GetProcessId();
|
||||
@@ -323,9 +351,11 @@ extern "C" void Discord_Initialize(const char *applicationId,
|
||||
};
|
||||
|
||||
IoThread->Start();
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_Shutdown(void) {
|
||||
extern "C" void Discord_Shutdown() {
|
||||
|
||||
if (!Connection) {
|
||||
return;
|
||||
}
|
||||
@@ -341,16 +371,19 @@ extern "C" void Discord_Shutdown(void) {
|
||||
}
|
||||
|
||||
RpcConnection::Destroy(Connection);
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
||||
QueuedPresence.length = JsonWriteRichPresenceObj(
|
||||
QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
|
||||
QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
|
||||
UpdatePresence.exchange(true);
|
||||
}
|
||||
|
||||
SignalIOActivity();
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_ClearPresence(void) {
|
||||
@@ -358,20 +391,22 @@ extern "C" void Discord_ClearPresence(void) {
|
||||
}
|
||||
|
||||
extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) {
|
||||
|
||||
// if we are not connected, let's not batch up stale messages for later
|
||||
if (!Connection || !Connection->IsOpen()) {
|
||||
return;
|
||||
}
|
||||
auto qmessage = SendQueue.GetNextAddMessage();
|
||||
if (qmessage) {
|
||||
qmessage->length =
|
||||
JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
|
||||
qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
|
||||
SendQueue.CommitAdd();
|
||||
SignalIOActivity();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_RunCallbacks(void) {
|
||||
extern "C" void Discord_RunCallbacks() {
|
||||
|
||||
// Note on some weirdness: internally we might connect, get other signals, disconnect any number
|
||||
// of times inbetween calls here. Externally, we want the sequence to seem sane, so any other
|
||||
// signals are book-ended by calls to ready and disconnect.
|
||||
@@ -380,8 +415,8 @@ extern "C" void Discord_RunCallbacks(void) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool wasDisconnected = WasJustDisconnected.exchange(false);
|
||||
bool isConnected = Connection->IsOpen();
|
||||
const bool wasDisconnected = WasJustDisconnected.exchange(false);
|
||||
const bool isConnected = Connection->IsOpen();
|
||||
|
||||
if (isConnected) {
|
||||
// if we are connected, disconnect cb first
|
||||
@@ -394,10 +429,7 @@ extern "C" void Discord_RunCallbacks(void) {
|
||||
if (WasJustConnected.exchange(false)) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.ready) {
|
||||
DiscordUser du { connectedUser.userId,
|
||||
connectedUser.username,
|
||||
connectedUser.discriminator,
|
||||
connectedUser.avatar };
|
||||
DiscordUser du { connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
||||
Handlers.ready(&du);
|
||||
}
|
||||
}
|
||||
@@ -429,7 +461,7 @@ extern "C" void Discord_RunCallbacks(void) {
|
||||
// maybe show them in one common dialog and/or start fetching the avatars in parallel, and if
|
||||
// not it should be trivial for the implementer to make a queue themselves.
|
||||
while (JoinAskQueue.HavePendingSends()) {
|
||||
auto req = JoinAskQueue.GetNextSendMessage();
|
||||
const auto req = JoinAskQueue.GetNextSendMessage();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
if (Handlers.joinRequest) {
|
||||
@@ -447,9 +479,11 @@ extern "C" void Discord_RunCallbacks(void) {
|
||||
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
||||
|
||||
if (newHandlers) {
|
||||
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
||||
@@ -472,8 +506,5 @@ extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
||||
Handlers = {};
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
94
3rdparty/discord-rpc/discord_rpc.h
vendored
Normal file
94
3rdparty/discord-rpc/discord_rpc.h
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_RPC_H
|
||||
#define DISCORD_RPC_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct DiscordRichPresence {
|
||||
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 */
|
||||
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;
|
||||
|
||||
typedef struct DiscordUser {
|
||||
const char *userId;
|
||||
const char *username;
|
||||
const char *discriminator;
|
||||
const char *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 (*joinRequest)(const DiscordUser *request);
|
||||
} DiscordEventHandlers;
|
||||
|
||||
#define DISCORD_REPLY_NO 0
|
||||
#define DISCORD_REPLY_YES 1
|
||||
#define DISCORD_REPLY_IGNORE 2
|
||||
#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);
|
||||
|
||||
// checks for incoming messages, dispatches callbacks
|
||||
void Discord_RunCallbacks(void);
|
||||
|
||||
void Discord_UpdatePresence(const DiscordRichPresence *presence);
|
||||
void Discord_ClearPresence(void);
|
||||
|
||||
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
|
||||
|
||||
void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif // DISCORD_RPC_H
|
||||
@@ -1,24 +1,52 @@
|
||||
#include "rpc_connection.h"
|
||||
#include "serialization.h"
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_rpc_connection.h"
|
||||
#include "discord_serialization.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
static const int RpcVersion = 1;
|
||||
static RpcConnection Instance;
|
||||
|
||||
/*static*/ RpcConnection *RpcConnection::Create(const char *applicationId) {
|
||||
RpcConnection *RpcConnection::Create(const char *applicationId) {
|
||||
|
||||
Instance.connection = BaseConnection::Create();
|
||||
StringCopy(Instance.appId, applicationId);
|
||||
return &Instance;
|
||||
|
||||
}
|
||||
|
||||
/*static*/ void RpcConnection::Destroy(RpcConnection *&c) {
|
||||
void RpcConnection::Destroy(RpcConnection *&c) {
|
||||
|
||||
c->Close();
|
||||
BaseConnection::Destroy(c->connection);
|
||||
c = nullptr;
|
||||
|
||||
}
|
||||
|
||||
void RpcConnection::Open() {
|
||||
|
||||
if (state == State::Connected) {
|
||||
return;
|
||||
}
|
||||
@@ -51,17 +79,21 @@ void RpcConnection::Open() {
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RpcConnection::Close() {
|
||||
|
||||
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
|
||||
onDisconnect(lastErrorCode, lastErrorMessage);
|
||||
}
|
||||
connection->Close();
|
||||
state = State::Disconnected;
|
||||
|
||||
}
|
||||
|
||||
bool RpcConnection::Write(const void *data, size_t length) {
|
||||
|
||||
sendFrame.opcode = Opcode::Frame;
|
||||
memcpy(sendFrame.message, data, length);
|
||||
sendFrame.length = static_cast<uint32_t>(length);
|
||||
@@ -69,14 +101,17 @@ bool RpcConnection::Write(const void *data, size_t length) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool RpcConnection::Read(JsonDocument &message) {
|
||||
|
||||
if (state != State::Connected && state != State::SentHandshake) {
|
||||
return false;
|
||||
}
|
||||
MessageFrame readFrame;
|
||||
MessageFrame readFrame{};
|
||||
for (;;) {
|
||||
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
|
||||
if (!didRead) {
|
||||
@@ -127,7 +162,7 @@ bool RpcConnection::Read(JsonDocument &message) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
88
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
Normal file
88
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_RPC_CONNECTION_H
|
||||
#define DISCORD_RPC_CONNECTION_H
|
||||
|
||||
#include "discord_connection.h"
|
||||
#include "discord_serialization.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much smaller.
|
||||
constexpr size_t MaxRpcFrameSize = 64 * 1024;
|
||||
|
||||
struct RpcConnection {
|
||||
enum class ErrorCode : int {
|
||||
Success = 0,
|
||||
PipeClosed = 1,
|
||||
ReadCorrupt = 2,
|
||||
};
|
||||
|
||||
enum class Opcode : uint32_t {
|
||||
Handshake = 0,
|
||||
Frame = 1,
|
||||
Close = 2,
|
||||
Ping = 3,
|
||||
Pong = 4,
|
||||
};
|
||||
|
||||
struct MessageFrameHeader {
|
||||
Opcode opcode;
|
||||
uint32_t length;
|
||||
};
|
||||
|
||||
struct MessageFrame : public MessageFrameHeader {
|
||||
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
|
||||
};
|
||||
|
||||
enum class State : uint32_t {
|
||||
Disconnected,
|
||||
SentHandshake,
|
||||
AwaitingResponse,
|
||||
Connected,
|
||||
};
|
||||
|
||||
BaseConnection *connection { nullptr };
|
||||
State state { State::Disconnected };
|
||||
void (*onConnect)(JsonDocument &message) { nullptr };
|
||||
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
|
||||
char appId[64] {};
|
||||
int lastErrorCode { 0 };
|
||||
char lastErrorMessage[256] {};
|
||||
RpcConnection::MessageFrame sendFrame;
|
||||
|
||||
static RpcConnection *Create(const char *applicationId);
|
||||
static void Destroy(RpcConnection *&);
|
||||
|
||||
inline bool IsOpen() const { return state == State::Connected; }
|
||||
|
||||
void Open();
|
||||
void Close();
|
||||
bool Write(const void *data, size_t length);
|
||||
bool Read(JsonDocument &message);
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif // DISCORD_RPC_CONNECTION_H
|
||||
@@ -1,11 +1,35 @@
|
||||
#include "serialization.h"
|
||||
#include "connection.h"
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "discord_serialization.h"
|
||||
#include "discord_connection.h"
|
||||
#include "discord_rpc.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
template<typename T>
|
||||
void NumberToString(char *dest, T number) {
|
||||
|
||||
if (!number) {
|
||||
*dest++ = '0';
|
||||
*dest++ = 0;
|
||||
@@ -26,6 +50,7 @@ void NumberToString(char *dest, T number) {
|
||||
*dest++ = temp[place];
|
||||
}
|
||||
*dest = 0;
|
||||
|
||||
}
|
||||
|
||||
// it's ever so slightly faster to not have to strlen the key
|
||||
@@ -62,24 +87,25 @@ struct WriteArray {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void JsonWriteNonce(JsonWriter &writer, int nonce) {
|
||||
static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
|
||||
|
||||
WriteKey(writer, "nonce");
|
||||
char nonceBuffer[32];
|
||||
NumberToString(nonceBuffer, nonce);
|
||||
writer.String(nonceBuffer);
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteRichPresenceObj(char *dest,
|
||||
size_t maxLen,
|
||||
int nonce,
|
||||
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);
|
||||
|
||||
{
|
||||
@@ -102,6 +128,9 @@ size_t JsonWriteRichPresenceObj(char *dest,
|
||||
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);
|
||||
}
|
||||
|
||||
WriteOptionalString(writer, "name", presence->name);
|
||||
@@ -168,6 +197,7 @@ size_t JsonWriteRichPresenceObj(char *dest,
|
||||
}
|
||||
|
||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
@@ -179,9 +209,11 @@ size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
@@ -197,9 +229,11 @@ size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const cha
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
@@ -215,9 +249,11 @@ size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const c
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce) {
|
||||
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
|
||||
|
||||
JsonWriter writer(dest, maxLen);
|
||||
|
||||
{
|
||||
@@ -243,7 +279,7 @@ size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int rep
|
||||
}
|
||||
|
||||
return writer.Size();
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
#pragma once
|
||||
/*
|
||||
* Copyright 2017 Discord, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DISCORD_SERIALIZATION_H
|
||||
#define DISCORD_SERIALIZATION_H
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/stringbuffer.h>
|
||||
#include <rapidjson/writer.h>
|
||||
|
||||
struct DiscordRichPresence;
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// if only there was a standard library function for this
|
||||
@@ -24,12 +50,7 @@ inline size_t StringCopy(char (&dest)[Len], const char *src) {
|
||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
|
||||
|
||||
// Commands
|
||||
struct DiscordRichPresence;
|
||||
size_t JsonWriteRichPresenceObj(char *dest,
|
||||
size_t maxLen,
|
||||
int nonce,
|
||||
int pid,
|
||||
const DiscordRichPresence *presence);
|
||||
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);
|
||||
@@ -149,35 +170,44 @@ class JsonDocument : public JsonDocumentBase {
|
||||
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) {
|
||||
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
|
||||
12
3rdparty/discord-rpc/include/discord_register.h
vendored
12
3rdparty/discord-rpc/include/discord_register.h
vendored
@@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void Discord_Register(const char* applicationId, const char* command);
|
||||
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
77
3rdparty/discord-rpc/include/discord_rpc.h
vendored
77
3rdparty/discord-rpc/include/discord_rpc.h
vendored
@@ -1,77 +0,0 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
|
||||
// clang-format on
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct DiscordRichPresence {
|
||||
int 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 */
|
||||
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;
|
||||
|
||||
typedef struct DiscordUser {
|
||||
const char* userId;
|
||||
const char* username;
|
||||
const char* discriminator;
|
||||
const char* 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 (*joinRequest)(const DiscordUser* request);
|
||||
} DiscordEventHandlers;
|
||||
|
||||
#define DISCORD_REPLY_NO 0
|
||||
#define DISCORD_REPLY_YES 1
|
||||
#define DISCORD_REPLY_IGNORE 2
|
||||
#define DISCORD_PARTY_PRIVATE 0
|
||||
#define DISCORD_PARTY_PUBLIC 1
|
||||
|
||||
void Discord_Initialize(const char* applicationId,
|
||||
DiscordEventHandlers* handlers,
|
||||
int autoRegister,
|
||||
const char* optionalSteamId);
|
||||
void Discord_Shutdown(void);
|
||||
|
||||
/* checks for incoming messages, dispatches callbacks */
|
||||
void Discord_RunCallbacks(void);
|
||||
|
||||
void Discord_UpdatePresence(const DiscordRichPresence* presence);
|
||||
void Discord_ClearPresence(void);
|
||||
|
||||
void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply);
|
||||
|
||||
void Discord_UpdateHandlers(DiscordEventHandlers* handlers);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
} // namespace discord_rpc
|
||||
|
||||
#endif
|
||||
41
3rdparty/discord-rpc/src/CMakeLists.txt
vendored
41
3rdparty/discord-rpc/src/CMakeLists.txt
vendored
@@ -1,41 +0,0 @@
|
||||
set(DISCORD_RPC_SOURCES
|
||||
../include/discord_rpc.h
|
||||
../include/discord_register.h
|
||||
discord_rpc.cpp
|
||||
rpc_connection.h
|
||||
rpc_connection.cpp
|
||||
serialization.h
|
||||
serialization.cpp
|
||||
connection.h
|
||||
backoff.h
|
||||
msg_queue.h
|
||||
)
|
||||
|
||||
if(UNIX)
|
||||
list(APPEND DISCORD_RPC_SOURCES connection_unix.cpp)
|
||||
if(APPLE)
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
|
||||
add_definitions(-DDISCORD_OSX)
|
||||
else()
|
||||
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
|
||||
add_definitions(-DDISCORD_LINUX)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND DISCORD_RPC_SOURCES connection_win.cpp discord_register_win.cpp)
|
||||
add_definitions(-DDISCORD_WINDOWS)
|
||||
endif()
|
||||
|
||||
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
||||
endif()
|
||||
|
||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
||||
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
|
||||
44
3rdparty/discord-rpc/src/backoff.h
vendored
44
3rdparty/discord-rpc/src/backoff.h
vendored
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
struct Backoff {
|
||||
int64_t minAmount;
|
||||
int64_t maxAmount;
|
||||
int64_t current;
|
||||
int fails;
|
||||
std::mt19937_64 randGenerator;
|
||||
std::uniform_real_distribution<> randDistribution;
|
||||
|
||||
double rand01() { return randDistribution(randGenerator); }
|
||||
|
||||
Backoff(int64_t min, int64_t max)
|
||||
: minAmount(min)
|
||||
, maxAmount(max)
|
||||
, current(min)
|
||||
, fails(0)
|
||||
, randGenerator(static_cast<uint64_t>(time(0)))
|
||||
{
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
fails = 0;
|
||||
current = minAmount;
|
||||
}
|
||||
|
||||
int64_t nextDelay()
|
||||
{
|
||||
++fails;
|
||||
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
|
||||
current = std::min(current + delay, maxAmount);
|
||||
return current;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
22
3rdparty/discord-rpc/src/connection.h
vendored
22
3rdparty/discord-rpc/src/connection.h
vendored
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// This is to wrap the platform specific kinds of connect/read/write.
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// not really connectiony, but need per-platform
|
||||
int GetProcessId();
|
||||
|
||||
struct BaseConnection {
|
||||
static BaseConnection *Create();
|
||||
static void Destroy(BaseConnection *&);
|
||||
bool isOpen { false };
|
||||
bool Open();
|
||||
bool Close();
|
||||
bool Write(const void *data, size_t length);
|
||||
bool Read(void *data, size_t length);
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
104
3rdparty/discord-rpc/src/discord_register_linux.cpp
vendored
104
3rdparty/discord-rpc/src/discord_register_linux.cpp
vendored
@@ -1,104 +0,0 @@
|
||||
#include "discord_rpc.h"
|
||||
#include "discord_register.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <errno.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
static bool Mkdir(const char *path) {
|
||||
int result = mkdir(path, 0755);
|
||||
if (result == 0) {
|
||||
return true;
|
||||
}
|
||||
if (errno == EEXIST) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // 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) {
|
||||
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
|
||||
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
char exePath[1024];
|
||||
if (!command || !command[0]) {
|
||||
ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
|
||||
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
|
||||
return;
|
||||
}
|
||||
exePath[size] = '\0';
|
||||
command = exePath;
|
||||
}
|
||||
|
||||
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
|
||||
"Name=Game %s\n"
|
||||
"Exec=%s %%u\n" // note: it really wants that %u in there
|
||||
"Type=Application\n"
|
||||
"NoDisplay=true\n"
|
||||
"Categories=Discord;Games;\n"
|
||||
"MimeType=x-scheme-handler/discord-%s;\n";
|
||||
char desktopFile[2048];
|
||||
int fileLen = snprintf(
|
||||
desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
|
||||
if (fileLen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char desktopFilename[256];
|
||||
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
|
||||
|
||||
char desktopFilePath[1024];
|
||||
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/share");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, "/applications");
|
||||
if (!Mkdir(desktopFilePath)) {
|
||||
return;
|
||||
}
|
||||
strcat(desktopFilePath, desktopFilename);
|
||||
|
||||
FILE *fp = fopen(desktopFilePath, "w");
|
||||
if (fp) {
|
||||
fwrite(desktopFile, 1, fileLen, fp);
|
||||
fclose(fp);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
char xdgMimeCommand[1024];
|
||||
snprintf(xdgMimeCommand,
|
||||
sizeof(xdgMimeCommand),
|
||||
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
|
||||
applicationId,
|
||||
applicationId);
|
||||
if (system(xdgMimeCommand) < 0) {
|
||||
fprintf(stderr, "Failed to register mime handler\n");
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void Discord_RegisterSteamGame(const char *applicationId,
|
||||
const char *steamId) {
|
||||
char command[256];
|
||||
sprintf(command, "xdg-open steam://rungameid/%s", steamId);
|
||||
Discord_Register(applicationId, command);
|
||||
}
|
||||
|
||||
80
3rdparty/discord-rpc/src/discord_register_osx.m
vendored
80
3rdparty/discord-rpc/src/discord_register_osx.m
vendored
@@ -1,80 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#include "discord_register.h"
|
||||
|
||||
static void RegisterCommand(const char* applicationId, const char* command)
|
||||
{
|
||||
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
|
||||
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
|
||||
// the command therein (will pass to js's window.open, so requires a url-like thing)
|
||||
|
||||
// Note: will not work for sandboxed apps
|
||||
NSString *home = NSHomeDirectory();
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
|
||||
stringByAppendingPathComponent:@"Application Support"]
|
||||
stringByAppendingPathComponent:@"discord"]
|
||||
stringByAppendingPathComponent:@"games"]
|
||||
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
|
||||
stringByAppendingPathExtension:@"json"];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
|
||||
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
|
||||
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
|
||||
}
|
||||
|
||||
static void RegisterURL(const char* applicationId)
|
||||
{
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "discord-%s", applicationId);
|
||||
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
|
||||
|
||||
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
|
||||
if (!myBundleId) {
|
||||
fprintf(stderr, "No bundle id found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
|
||||
if (!myURL) {
|
||||
fprintf(stderr, "No bundle url found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
|
||||
if (status != noErr) {
|
||||
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
|
||||
}
|
||||
}
|
||||
|
||||
void Discord_Register(const char* applicationId, const char* command)
|
||||
{
|
||||
if (command) {
|
||||
RegisterCommand(applicationId, command);
|
||||
}
|
||||
else {
|
||||
// raii lite
|
||||
@autoreleasepool {
|
||||
RegisterURL(applicationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Discord_RegisterSteamGame(const char* applicationId, const char* steamId)
|
||||
{
|
||||
char command[256];
|
||||
snprintf(command, 256, "steam://rungameid/%s", steamId);
|
||||
Discord_Register(applicationId, command);
|
||||
}
|
||||
40
3rdparty/discord-rpc/src/msg_queue.h
vendored
40
3rdparty/discord-rpc/src/msg_queue.h
vendored
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
|
||||
// a consumer. Mutex up as needed.
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
template <typename ElementType, std::size_t QueueSize>
|
||||
class MsgQueue {
|
||||
ElementType queue_[QueueSize];
|
||||
std::atomic_uint nextAdd_{0};
|
||||
std::atomic_uint nextSend_{0};
|
||||
std::atomic_uint pendingSends_{0};
|
||||
|
||||
public:
|
||||
MsgQueue() {}
|
||||
|
||||
ElementType* GetNextAddMessage()
|
||||
{
|
||||
// if we are falling behind, bail
|
||||
if (pendingSends_.load() >= QueueSize) {
|
||||
return nullptr;
|
||||
}
|
||||
auto index = (nextAdd_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitAdd() { ++pendingSends_; }
|
||||
|
||||
bool HavePendingSends() const { return pendingSends_.load() != 0; }
|
||||
ElementType* GetNextSendMessage()
|
||||
{
|
||||
auto index = (nextSend_++) % QueueSize;
|
||||
return &queue_[index];
|
||||
}
|
||||
void CommitSend() { --pendingSends_; }
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
64
3rdparty/discord-rpc/src/rpc_connection.h
vendored
64
3rdparty/discord-rpc/src/rpc_connection.h
vendored
@@ -1,64 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "connection.h"
|
||||
#include "serialization.h"
|
||||
|
||||
namespace discord_rpc {
|
||||
|
||||
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much
|
||||
// smaller.
|
||||
constexpr size_t MaxRpcFrameSize = 64 * 1024;
|
||||
|
||||
struct RpcConnection {
|
||||
enum class ErrorCode : int {
|
||||
Success = 0,
|
||||
PipeClosed = 1,
|
||||
ReadCorrupt = 2,
|
||||
};
|
||||
|
||||
enum class Opcode : uint32_t {
|
||||
Handshake = 0,
|
||||
Frame = 1,
|
||||
Close = 2,
|
||||
Ping = 3,
|
||||
Pong = 4,
|
||||
};
|
||||
|
||||
struct MessageFrameHeader {
|
||||
Opcode opcode;
|
||||
uint32_t length;
|
||||
};
|
||||
|
||||
struct MessageFrame : public MessageFrameHeader {
|
||||
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
|
||||
};
|
||||
|
||||
enum class State : uint32_t {
|
||||
Disconnected,
|
||||
SentHandshake,
|
||||
AwaitingResponse,
|
||||
Connected,
|
||||
};
|
||||
|
||||
BaseConnection *connection { nullptr };
|
||||
State state { State::Disconnected };
|
||||
void (*onConnect)(JsonDocument &message) { nullptr };
|
||||
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
|
||||
char appId[64] {};
|
||||
int lastErrorCode { 0 };
|
||||
char lastErrorMessage[256] {};
|
||||
RpcConnection::MessageFrame sendFrame;
|
||||
|
||||
static RpcConnection *Create(const char *applicationId);
|
||||
static void Destroy(RpcConnection *&);
|
||||
|
||||
inline bool IsOpen() const { return state == State::Connected; }
|
||||
|
||||
void Open();
|
||||
void Close();
|
||||
bool Write(const void *data, size_t length);
|
||||
bool Read(JsonDocument &message);
|
||||
};
|
||||
|
||||
} // namespace discord_rpc
|
||||
|
||||
@@ -259,7 +259,16 @@ if(APPLE)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
find_package(getopt-win REQUIRED)
|
||||
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)
|
||||
else()
|
||||
message(FATAL_ERROR "Missing getopt")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(APPLE OR WIN32)
|
||||
@@ -1494,7 +1503,7 @@ endif()
|
||||
|
||||
if(HAVE_DISCORD_RPC)
|
||||
add_subdirectory(3rdparty/discord-rpc)
|
||||
target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc/include)
|
||||
target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc)
|
||||
endif()
|
||||
|
||||
if(HAVE_TRANSLATIONS)
|
||||
@@ -1554,9 +1563,10 @@ 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:${WIN32}>:dsound dwmapi getopt-win::getopt>
|
||||
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
|
||||
$<$<BOOL:${MSVC}>:WindowsApp>
|
||||
KDAB::kdsingleapplication
|
||||
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
@@ -1575,10 +1585,6 @@ 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)
|
||||
|
||||
62
Changelog
62
Changelog
@@ -2,6 +2,68 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
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:
|
||||
|
||||
@@ -53,7 +53,7 @@ Funding developers is a way to contribute to open source projects you appreciate
|
||||
* 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/)
|
||||
* 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/), [elyrics.net](https://www.elyrics.net/), [letras.mus.br](https://www.letras.mus.br) and [LyricFind](https://lyrics.lyricfind.com)
|
||||
* Support for multiple backends
|
||||
* Audio analyzer
|
||||
* Audio equalizer
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||
set(STRAWBERRY_VERSION_MINOR 2)
|
||||
set(STRAWBERRY_VERSION_PATCH 9)
|
||||
set(STRAWBERRY_VERSION_PATCH 13)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<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>
|
||||
|
||||
@@ -12,9 +12,13 @@ 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,
|
||||
@@ -22,7 +26,9 @@ 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,
|
||||
@@ -86,7 +92,11 @@ CREATE TABLE device_%deviceid_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -94,4 +104,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=5 WHERE ROWID=%deviceid;
|
||||
UPDATE devices SET schema_version=6 WHERE ROWID=%deviceid;
|
||||
|
||||
43
data/schema/schema-21.sql
Normal file
43
data/schema/schema-21.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
||||
|
||||
DELETE FROM schema_version;
|
||||
|
||||
INSERT INTO schema_version (version) VALUES (20);
|
||||
INSERT INTO schema_version (version) VALUES (21);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS directories (
|
||||
path TEXT NOT NULL,
|
||||
@@ -20,9 +20,13 @@ 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,
|
||||
@@ -30,7 +34,9 @@ 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,
|
||||
@@ -94,16 +100,24 @@ CREATE TABLE IF NOT EXISTS songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -111,7 +125,9 @@ 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,
|
||||
@@ -175,16 +191,24 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -192,7 +216,9 @@ 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,
|
||||
@@ -256,16 +282,24 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -273,7 +307,9 @@ 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,
|
||||
@@ -337,16 +373,24 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -354,7 +398,9 @@ 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,
|
||||
@@ -418,16 +464,24 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -435,7 +489,9 @@ 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,
|
||||
@@ -499,16 +555,24 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -516,7 +580,9 @@ 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,
|
||||
@@ -580,16 +646,24 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -597,7 +671,9 @@ 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,
|
||||
@@ -661,16 +737,24 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -678,7 +762,9 @@ 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,
|
||||
@@ -742,16 +828,24 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -759,7 +853,9 @@ 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,
|
||||
@@ -823,16 +919,24 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -840,7 +944,9 @@ 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,
|
||||
@@ -904,7 +1010,11 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -931,9 +1041,13 @@ 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,
|
||||
@@ -941,7 +1055,9 @@ 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,
|
||||
@@ -1005,7 +1121,11 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
||||
musicbrainz_work_id TEXT,
|
||||
|
||||
ebur128_integrated_loudness_lufs REAL,
|
||||
ebur128_loudness_range_lu REAL
|
||||
ebur128_loudness_range_lu REAL,
|
||||
|
||||
bpm REAL,
|
||||
mood TEXT,
|
||||
initial_key TEXT
|
||||
|
||||
);
|
||||
|
||||
@@ -1032,10 +1152,22 @@ 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;
|
||||
|
||||
2
debian/control
vendored
2
debian/control
vendored
@@ -60,7 +60,7 @@ Description: music player and music collection organizer
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<li>Edit tags on audio files</li>
|
||||
<li>Automatically retrieve tags from MusicBrainz</li>
|
||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</li>
|
||||
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind</li>
|
||||
<li>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, Libre.fm and ListenBrainz</li>
|
||||
@@ -51,6 +51,10 @@
|
||||
</screenshots>
|
||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||
<releases>
|
||||
<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"/>
|
||||
|
||||
@@ -13,8 +13,7 @@ TryExec=strawberry
|
||||
Icon=strawberry
|
||||
Terminal=false
|
||||
Categories=AudioVideo;Player;Qt;Audio;
|
||||
Keywords=Audio;Player;
|
||||
StartupNotify=false
|
||||
Keywords=Audio;Player;Clementine;
|
||||
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;
|
||||
|
||||
2
dist/unix/strawberry.1
vendored
2
dist/unix/strawberry.1
vendored
@@ -29,7 +29,7 @@ Features:
|
||||
.br
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
.br
|
||||
- Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
||||
.br
|
||||
- Support for multiple backends
|
||||
.br
|
||||
|
||||
2
dist/unix/strawberry.spec.in
vendored
2
dist/unix/strawberry.spec.in
vendored
@@ -93,7 +93,7 @@ Features:
|
||||
- Edit tags on audio files
|
||||
- Automatically retrieve tags from MusicBrainz
|
||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com, elyrics.net, letras.mus.br and LyricFind
|
||||
- Support for multiple backends
|
||||
- Audio analyzer
|
||||
- Audio equalizer
|
||||
|
||||
95
dist/windows/strawberry.nsi.in
vendored
95
dist/windows/strawberry.nsi.in
vendored
@@ -21,6 +21,10 @@
|
||||
!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
|
||||
@@ -31,6 +35,10 @@
|
||||
!define arch "x64"
|
||||
!endif
|
||||
|
||||
!ifdef arch_arm64
|
||||
!define arch "arm64"
|
||||
!endif
|
||||
|
||||
|
||||
!if "@CMAKE_BUILD_TYPE@" == "Release"
|
||||
!define release
|
||||
@@ -38,6 +46,8 @@
|
||||
!define release
|
||||
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
|
||||
!define debug
|
||||
!else
|
||||
!error "Missing CMAKE_BUILD_TYPE"
|
||||
!endif
|
||||
|
||||
!ifdef release
|
||||
@@ -70,7 +80,7 @@
|
||||
!ifdef arch_x86
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
!ifdef arch_x64 || arch_arm64
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug"
|
||||
!endif
|
||||
!else
|
||||
@@ -80,7 +90,7 @@
|
||||
!ifdef arch_x86
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
!ifdef arch_x64 || arch_arm64
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player"
|
||||
!endif
|
||||
!endif
|
||||
@@ -214,7 +224,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'
|
||||
ExecWait '"$TEMP\${vc_redist_file}" /install /passive /norestart'
|
||||
Delete "$TEMP\${vc_redist_file}"
|
||||
SetDetailsView show
|
||||
; ${EndIf}
|
||||
@@ -324,7 +334,7 @@ Section "Strawberry" Strawberry
|
||||
File "libqtsparkle-qt6.dll"
|
||||
File "libsoup-3.0-0.dll"
|
||||
File "libspeex-1.dll"
|
||||
File "libsqlite3.dll"
|
||||
File "libsqlite3-0.dll"
|
||||
File "libssp-0.dll"
|
||||
File "libstdc++-6.dll"
|
||||
File "libtag.dll"
|
||||
@@ -367,6 +377,10 @@ 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"
|
||||
@@ -381,7 +395,9 @@ Section "Strawberry" Strawberry
|
||||
File "glib-2.0-0.dll"
|
||||
File "gme.dll"
|
||||
File "gmodule-2.0-0.dll"
|
||||
!ifndef arch_arm64
|
||||
File "gnutls.dll"
|
||||
!endif
|
||||
File "gobject-2.0-0.dll"
|
||||
File "gstadaptivedemux-1.0-0.dll"
|
||||
File "gstapp-1.0-0.dll"
|
||||
@@ -402,13 +418,17 @@ Section "Strawberry" Strawberry
|
||||
File "gsttag-1.0-0.dll"
|
||||
File "gsturidownloader-1.0-0.dll"
|
||||
File "gstvideo-1.0-0.dll"
|
||||
!ifdef arch_arm64
|
||||
File "gstwinrt-1.0-0.dll"
|
||||
!endif
|
||||
File "harfbuzz.dll"
|
||||
File "intl-8.dll"
|
||||
File "jpeg62.dll"
|
||||
File "kdsingleapplication-qt6.dll"
|
||||
File "libbs2b.dll"
|
||||
!ifndef arch_arm64
|
||||
File "libfaac_dll.dll"
|
||||
!endif
|
||||
File "liblzma.dll"
|
||||
File "libmp3lame.dll"
|
||||
File "libopenmpt.dll"
|
||||
@@ -434,8 +454,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeex.dll"
|
||||
File "pcre2-8.dll"
|
||||
File "pcre2-16.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolame.dll"
|
||||
File "zlib.dll"
|
||||
!endif
|
||||
File "zlib1.dll"
|
||||
!endif
|
||||
!ifdef debug
|
||||
File "freetyped.dll"
|
||||
@@ -444,8 +466,10 @@ Section "Strawberry" Strawberry
|
||||
File "libspeexd.dll"
|
||||
File "pcre2-8d.dll"
|
||||
File "pcre2-16d.dll"
|
||||
!ifndef arch_arm64
|
||||
File "twolamed.dll"
|
||||
File "zlibd.dll"
|
||||
!endif
|
||||
File "zlibd1.dll"
|
||||
!endif
|
||||
|
||||
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
|
||||
@@ -459,7 +483,11 @@ Section "Strawberry" Strawberry
|
||||
; Common files
|
||||
|
||||
File "icudt77.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
File "fftw3.dll"
|
||||
!else
|
||||
File "libfftw3-3.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
File "icuin77d.dll"
|
||||
File "icuuc77d.dll"
|
||||
@@ -526,11 +554,13 @@ 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
|
||||
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
|
||||
!ifdef arch_arm64
|
||||
File "/oname=gioopenssl.dll" "gio-modules\gioopenssl.dll"
|
||||
!else
|
||||
File "/oname=giognutls.dll" "gio-modules\giognutls.dll"
|
||||
!endif
|
||||
!endif
|
||||
SectionEnd
|
||||
|
||||
@@ -674,7 +704,6 @@ 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"
|
||||
@@ -707,7 +736,6 @@ 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"
|
||||
@@ -719,8 +747,12 @@ 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
|
||||
|
||||
@@ -849,7 +881,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libqtsparkle-qt6.dll"
|
||||
Delete "$INSTDIR\libsoup-3.0-0.dll"
|
||||
Delete "$INSTDIR\libspeex-1.dll"
|
||||
Delete "$INSTDIR\libsqlite3.dll"
|
||||
Delete "$INSTDIR\libsqlite3-0.dll"
|
||||
Delete "$INSTDIR\libssp-0.dll"
|
||||
Delete "$INSTDIR\libstdc++-6.dll"
|
||||
Delete "$INSTDIR\libtag.dll"
|
||||
@@ -892,6 +924,10 @@ 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"
|
||||
@@ -906,7 +942,9 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\glib-2.0-0.dll"
|
||||
Delete "$INSTDIR\gme.dll"
|
||||
Delete "$INSTDIR\gmodule-2.0-0.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\gnutls.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\gobject-2.0-0.dll"
|
||||
Delete "$INSTDIR\gstadaptivedemux-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstapp-1.0-0.dll"
|
||||
@@ -927,13 +965,17 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gsttag-1.0-0.dll"
|
||||
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
|
||||
Delete "$INSTDIR\gstvideo-1.0-0.dll"
|
||||
!ifdef arch_arm64
|
||||
Delete "$INSTDIR\gstwinrt-1.0-0.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\harfbuzz.dll"
|
||||
Delete "$INSTDIR\intl-8.dll"
|
||||
Delete "$INSTDIR\jpeg62.dll"
|
||||
Delete "$INSTDIR\kdsingleapplication-qt6.dll"
|
||||
Delete "$INSTDIR\libbs2b.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\libfaac_dll.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\liblzma.dll"
|
||||
Delete "$INSTDIR\libmp3lame.dll"
|
||||
Delete "$INSTDIR\libopenmpt.dll"
|
||||
@@ -959,8 +1001,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeex.dll"
|
||||
Delete "$INSTDIR\pcre2-8.dll"
|
||||
Delete "$INSTDIR\pcre2-16.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolame.dll"
|
||||
Delete "$INSTDIR\zlib.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
!endif
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\freetyped.dll"
|
||||
@@ -969,8 +1013,10 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libspeexd.dll"
|
||||
Delete "$INSTDIR\pcre2-8d.dll"
|
||||
Delete "$INSTDIR\pcre2-16d.dll"
|
||||
!ifndef arch_arm64
|
||||
Delete "$INSTDIR\twolamed.dll"
|
||||
Delete "$INSTDIR\zlibd.dll"
|
||||
!endif
|
||||
Delete "$INSTDIR\zlibd1.dll"
|
||||
!endif
|
||||
|
||||
!ifdef arch_x86
|
||||
@@ -983,7 +1029,11 @@ Section "Uninstall"
|
||||
; Common files
|
||||
|
||||
Delete "$INSTDIR\icudt77.dll"
|
||||
!ifdef msvc && arch_arm64
|
||||
Delete "$INSTDIR\fftw3.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\libfftw3-3.dll"
|
||||
!endif
|
||||
!ifdef msvc && debug
|
||||
Delete "$INSTDIR\icuin77d.dll"
|
||||
Delete "$INSTDIR\icuuc77d.dll"
|
||||
@@ -1016,11 +1066,13 @@ Section "Uninstall"
|
||||
|
||||
!ifdef mingw
|
||||
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
|
||||
Delete "$INSTDIR\gio-modules\libgioopenssl.dll"
|
||||
!endif
|
||||
!ifdef msvc
|
||||
Delete "$INSTDIR\gio-modules\giognutls.dll"
|
||||
!ifdef arch_arm64
|
||||
Delete "$INSTDIR\gio-modules\gioopenssl.dll"
|
||||
!else
|
||||
Delete "$INSTDIR\gio-modules\giognutls.dll"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!ifdef msvc && debug
|
||||
@@ -1133,7 +1185,6 @@ 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"
|
||||
@@ -1166,7 +1217,6 @@ 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"
|
||||
@@ -1178,9 +1228,14 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"
|
||||
Delete "$INSTDIR\gstreamer-plugins\gstxingmux.dll"
|
||||
!ifdef arch_x64
|
||||
;Delete "$INSTDIR\gstreamer-plugins\gstspotify.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"
|
||||
!endif
|
||||
|
||||
!endif ; msvc
|
||||
|
||||
Delete "$INSTDIR\Uninstall.exe"
|
||||
|
||||
@@ -623,6 +623,7 @@ 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;
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QRegularExpression>
|
||||
#include <QInputDialog>
|
||||
#include <QList>
|
||||
@@ -295,19 +296,21 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
|
||||
if (version == 1) {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(name).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
ds >> g;
|
||||
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
|
||||
ret->addAction(CreateGroupByAction(QUrl::fromPercentEncoding(name.toUtf8()), parent, g));
|
||||
}
|
||||
}
|
||||
else {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
s.remove(saved.at(i));
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
s.remove(name);
|
||||
}
|
||||
}
|
||||
s.endGroup();
|
||||
@@ -339,7 +342,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
||||
|
||||
if (!model_) return;
|
||||
|
||||
QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
const QString name = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
if (name.isEmpty()) return;
|
||||
|
||||
qLog(Debug) << "Saving current grouping to" << name;
|
||||
@@ -355,7 +358,7 @@ void CollectionFilterWidget::SaveGroupBy() {
|
||||
QDataStream datastream(&buffer, QIODevice::WriteOnly);
|
||||
datastream << model_->GetGroupBy();
|
||||
s.setValue("version", u"1"_s);
|
||||
s.setValue(name, buffer);
|
||||
s.setValue(QUrl::toPercentEncoding(name), buffer);
|
||||
s.endGroup();
|
||||
|
||||
UpdateGroupByActions();
|
||||
|
||||
@@ -78,6 +78,8 @@ 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();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -54,6 +54,7 @@
|
||||
|
||||
#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"
|
||||
@@ -71,12 +72,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");
|
||||
@@ -88,7 +89,6 @@ 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,10 +130,6 @@ 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);
|
||||
@@ -191,13 +187,9 @@ void CollectionModel::EndReset() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::Reload() {
|
||||
void CollectionModel::ResetInternal() {
|
||||
|
||||
loading_ = true;
|
||||
if (timer_reload_->isActive()) {
|
||||
timer_reload_->stop();
|
||||
}
|
||||
updates_.clear();
|
||||
|
||||
options_active_ = options_current_;
|
||||
|
||||
@@ -211,22 +203,15 @@ void CollectionModel::Reload() {
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ScheduleReset() {
|
||||
|
||||
if (!timer_reload_->isActive()) {
|
||||
timer_reload_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ReloadSettings() {
|
||||
|
||||
Settings settings;
|
||||
settings.beginGroup(CollectionSettings::kSettingsGroup);
|
||||
const bool show_pretty_covers = settings.value(CollectionSettings::kPrettyCovers, true).toBool();
|
||||
const bool show_dividers= settings.value(CollectionSettings::kShowDividers, true).toBool();
|
||||
const bool show_dividers = settings.value(CollectionSettings::kShowDividers, true).toBool();
|
||||
const bool show_various_artists = settings.value(CollectionSettings::kVariousArtists, true).toBool();
|
||||
const bool sort_skips_articles = settings.value(CollectionSettings::kSortSkipsArticles, true).toBool();
|
||||
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();
|
||||
|
||||
use_disk_cache_ = settings.value(CollectionSettings::kSettingsDiskCacheEnable, false).toBool();
|
||||
QPixmapCache::setCacheLimit(static_cast<int>(MaximumCacheSize(&settings, CollectionSettings::kSettingsCacheSize, CollectionSettings::kSettingsCacheSizeUnit, CollectionSettings::kSettingsCacheSizeDefault) / 1024));
|
||||
@@ -241,11 +226,13 @@ 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_skips_articles != options_current_.sort_skips_articles) {
|
||||
sort_skip_articles_for_artists != options_current_.sort_skip_articles_for_artists ||
|
||||
sort_skip_articles_for_albums != options_current_.sort_skip_articles_for_albums) {
|
||||
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_skips_articles = sort_skips_articles;
|
||||
options_current_.sort_skip_articles_for_artists = sort_skip_articles_for_artists;
|
||||
options_current_.sort_skip_articles_for_albums = sort_skip_articles_for_albums;
|
||||
ScheduleReset();
|
||||
}
|
||||
|
||||
@@ -421,10 +408,15 @@ void CollectionModel::RemoveSongs(const SongList &songs) {
|
||||
|
||||
void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs) {
|
||||
|
||||
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 (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()) {
|
||||
@@ -433,6 +425,12 @@ void CollectionModel::ScheduleUpdate(const CollectionModelUpdate::Type type, con
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ScheduleReset() {
|
||||
|
||||
ScheduleUpdate(CollectionModelUpdate::Type::Reset);
|
||||
|
||||
}
|
||||
|
||||
void CollectionModel::ScheduleAddSongs(const SongList &songs) {
|
||||
|
||||
ScheduleUpdate(CollectionModelUpdate::Type::Add, songs);
|
||||
@@ -465,6 +463,9 @@ void CollectionModel::ProcessUpdate() {
|
||||
}
|
||||
|
||||
switch (update.type) {
|
||||
case CollectionModelUpdate::Type::Reset:
|
||||
ResetInternal();
|
||||
break;
|
||||
case CollectionModelUpdate::Type::AddReAddOrUpdate:
|
||||
AddReAddOrUpdateSongsInternal(update.songs);
|
||||
break;
|
||||
@@ -699,7 +700,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
||||
|
||||
QString divider_key;
|
||||
if (options_active_.show_dividers && container_level == 0) {
|
||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skips_articles));
|
||||
divider_key = DividerKey(group_by, song, SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums));
|
||||
if (!divider_key.isEmpty()) {
|
||||
if (!divider_nodes_.contains(divider_key)) {
|
||||
CreateDividerItem(divider_key, DividerDisplayText(group_by, divider_key), parent);
|
||||
@@ -713,7 +714,7 @@ CollectionItem *CollectionModel::CreateContainerItem(const GroupBy group_by, con
|
||||
item->container_level = container_level;
|
||||
item->container_key = container_key;
|
||||
item->display_text = DisplayText(group_by, song);
|
||||
item->sort_text = SortText(group_by, song, options_active_.sort_skips_articles);
|
||||
item->sort_text = SortText(group_by, song, options_active_.sort_skip_articles_for_artists, options_active_.sort_skip_articles_for_albums);
|
||||
if (!divider_key.isEmpty()) {
|
||||
item->sort_text.prepend(divider_key + QLatin1Char(' '));
|
||||
}
|
||||
@@ -1068,25 +1069,25 @@ QString CollectionModel::PrettyFormat(const Song &song) {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles) {
|
||||
QString CollectionModel::SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums) {
|
||||
|
||||
switch (group_by) {
|
||||
case GroupBy::AlbumArtist:
|
||||
return SortTextForArtist(song.effective_albumartist(), sort_skips_articles);
|
||||
return SortTextForName(song.effective_albumartistsort(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Artist:
|
||||
return SortTextForArtist(song.artist(), sort_skips_articles);
|
||||
return SortTextForName(song.effective_artistsort(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Album:
|
||||
return SortText(song.album());
|
||||
return SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
|
||||
case GroupBy::AlbumDisc:
|
||||
return song.album() + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::YearAlbum:
|
||||
return SortTextForNumber(std::max(0, song.year())) + song.grouping() + song.album();
|
||||
return SortTextForNumber(std::max(0, song.year())) + song.grouping() + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
|
||||
case GroupBy::YearAlbumDisc:
|
||||
return SortTextForNumber(std::max(0, song.year())) + song.album() + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForNumber(std::max(0, song.year())) + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::OriginalYearAlbum:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.grouping() + song.album();
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.grouping() + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums);
|
||||
case GroupBy::OriginalYearAlbumDisc:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + song.album() + SortTextForNumber(std::max(0, song.disc()));
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + SortTextForName(song.effective_albumsort(), sort_skip_articles_for_albums) + SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::Disc:
|
||||
return SortTextForNumber(std::max(0, song.disc()));
|
||||
case GroupBy::Year:
|
||||
@@ -1094,13 +1095,13 @@ QString CollectionModel::SortText(const GroupBy group_by, const Song &song, cons
|
||||
case GroupBy::OriginalYear:
|
||||
return SortTextForNumber(std::max(0, song.effective_originalyear())) + QLatin1Char(' ');
|
||||
case GroupBy::Genre:
|
||||
return SortTextForArtist(song.genre(), sort_skips_articles);
|
||||
return SortText(song.genre());
|
||||
case GroupBy::Composer:
|
||||
return SortTextForArtist(song.composer(), sort_skips_articles);
|
||||
return SortTextForName(song.effective_composersort(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Performer:
|
||||
return SortTextForArtist(song.performer(), sort_skips_articles);
|
||||
return SortTextForName(song.effective_performersort(), sort_skip_articles_for_artists);
|
||||
case GroupBy::Grouping:
|
||||
return SortTextForArtist(song.grouping(), sort_skips_articles);
|
||||
return SortText(song.grouping());
|
||||
case GroupBy::FileType:
|
||||
return song.TextForFiletype();
|
||||
case GroupBy::Format:
|
||||
@@ -1135,21 +1136,9 @@ QString CollectionModel::SortText(QString text) {
|
||||
|
||||
}
|
||||
|
||||
QString CollectionModel::SortTextForArtist(QString artist, const bool skip_articles) {
|
||||
QString CollectionModel::SortTextForName(const QString &name, const bool sort_skip_articles) {
|
||||
|
||||
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;
|
||||
return sort_skip_articles ? SkipArticles(SortText(name)) : SortText(name);
|
||||
|
||||
}
|
||||
|
||||
@@ -1180,6 +1169,20 @@ QString CollectionModel::SortTextForBitrate(const int bitrate) {
|
||||
|
||||
}
|
||||
|
||||
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() ||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -129,14 +129,16 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
show_dividers(true),
|
||||
show_pretty_covers(true),
|
||||
show_various_artists(true),
|
||||
sort_skips_articles(true),
|
||||
sort_skip_articles_for_artists(false),
|
||||
sort_skip_articles_for_albums(false),
|
||||
separate_albums_by_grouping(false) {}
|
||||
|
||||
Grouping group_by;
|
||||
bool show_dividers;
|
||||
bool show_pretty_covers;
|
||||
bool show_various_artists;
|
||||
bool sort_skips_articles;
|
||||
bool sort_skip_articles_for_artists;
|
||||
bool sort_skip_articles_for_albums;
|
||||
bool separate_albums_by_grouping;
|
||||
CollectionFilterOptions filter_options;
|
||||
};
|
||||
@@ -176,20 +178,21 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
|
||||
// Utility functions for manipulating text
|
||||
static QString DisplayText(const GroupBy group_by, const Song &song);
|
||||
QString DisplayText(const GroupBy group_by, const Song &song);
|
||||
static QString TextOrUnknown(const QString &text);
|
||||
static QString PrettyYearAlbum(const int year, const QString &album);
|
||||
static QString PrettyAlbumDisc(const QString &album, const int disc);
|
||||
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
|
||||
static QString PrettyDisc(const int disc);
|
||||
static QString PrettyFormat(const Song &song);
|
||||
QString SortText(const GroupBy group_by, const Song &song, const bool sort_skips_articles);
|
||||
QString SortText(const GroupBy group_by, const Song &song, const bool sort_skip_articles_for_artists, const bool sort_skip_articles_for_albums);
|
||||
static QString SortText(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;
|
||||
|
||||
@@ -228,7 +231,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
|
||||
QVariant data(CollectionItem *item, const int role) const;
|
||||
|
||||
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
|
||||
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs = SongList());
|
||||
void ScheduleAddSongs(const SongList &songs);
|
||||
void ScheduleUpdateSongs(const SongList &songs);
|
||||
void ScheduleRemoveSongs(const SongList &songs);
|
||||
@@ -259,7 +262,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 Reload();
|
||||
void ResetInternal();
|
||||
void ScheduleReset();
|
||||
void ProcessUpdate();
|
||||
void LoadSongsFromSqlAsyncFinished();
|
||||
@@ -278,7 +281,6 @@ 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_;
|
||||
|
||||
@@ -25,12 +25,13 @@
|
||||
class CollectionModelUpdate {
|
||||
public:
|
||||
enum class Type {
|
||||
Reset,
|
||||
AddReAddOrUpdate,
|
||||
Add,
|
||||
Update,
|
||||
Remove,
|
||||
};
|
||||
explicit CollectionModelUpdate(const Type _type, const SongList &_songs);
|
||||
explicit CollectionModelUpdate(const Type _type, const SongList &_songs = SongList());
|
||||
Type type;
|
||||
SongList songs;
|
||||
};
|
||||
|
||||
@@ -34,8 +34,6 @@ 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;
|
||||
@@ -62,7 +60,7 @@ void CollectionPlaylistItem::Reload() {
|
||||
qLog(Error) << "Could not reload file" << song_.url() << result.error_string();
|
||||
return;
|
||||
}
|
||||
UpdateTemporaryMetadata(song_);
|
||||
UpdateStreamMetadata(song_);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -78,16 +76,9 @@ 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 (HasTemporaryMetadata()) temp_metadata_.set_art_manual(cover_url);
|
||||
if (HasStreamMetadata()) stream_song_.set_art_manual(cover_url);
|
||||
|
||||
}
|
||||
|
||||
@@ -35,19 +35,17 @@ class CollectionPlaylistItem : public PlaylistItem {
|
||||
explicit CollectionPlaylistItem(const Song::Source source);
|
||||
explicit CollectionPlaylistItem(const Song &song);
|
||||
|
||||
QUrl Url() const override;
|
||||
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; }
|
||||
|
||||
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_); }
|
||||
|
||||
@@ -65,10 +65,8 @@
|
||||
#include "collectionitem.h"
|
||||
#include "collectionitemdelegate.h"
|
||||
#include "collectionview.h"
|
||||
#ifndef Q_OS_WIN32
|
||||
# include "device/devicemanager.h"
|
||||
# include "device/devicestatefiltermodel.h"
|
||||
#endif
|
||||
#include "device/devicemanager.h"
|
||||
#include "device/devicestatefiltermodel.h"
|
||||
#include "dialogs/edittagdialog.h"
|
||||
#include "dialogs/deleteconfirmationdialog.h"
|
||||
#include "organize/organizedialog.h"
|
||||
@@ -95,9 +93,7 @@ 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),
|
||||
@@ -417,9 +413,7 @@ 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();
|
||||
@@ -439,10 +433,8 @@ 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
|
||||
|
||||
}
|
||||
|
||||
@@ -481,9 +473,7 @@ 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_);
|
||||
|
||||
@@ -492,9 +482,7 @@ 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_);
|
||||
|
||||
@@ -759,7 +747,6 @@ void CollectionView::RescanSongs() {
|
||||
|
||||
void CollectionView::CopyToDevice() {
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
if (!organize_dialog_) {
|
||||
organize_dialog_ = make_unique<OrganizeDialog>(task_manager_, tagreader_client_, nullptr, this);
|
||||
}
|
||||
@@ -768,7 +755,6 @@ void CollectionView::CopyToDevice() {
|
||||
organize_dialog_->SetCopy(true);
|
||||
organize_dialog_->SetSongs(GetSelectedSongs());
|
||||
organize_dialog_->show();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ 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);
|
||||
@@ -176,9 +175,7 @@ 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_;
|
||||
|
||||
@@ -999,6 +999,18 @@ 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;
|
||||
@@ -1034,6 +1046,9 @@ 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.";
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QIODevice>
|
||||
#include <QDataStream>
|
||||
#include <QKeySequence>
|
||||
@@ -167,14 +168,20 @@ void SavedGroupingManager::UpdateModel() {
|
||||
if (version == 1) {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
QByteArray bytes = s.value(name).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
ds >> g;
|
||||
|
||||
QList<QStandardItem*> list;
|
||||
list << new QStandardItem(saved.at(i))
|
||||
|
||||
QStandardItem *item = new QStandardItem();
|
||||
item->setText(QUrl::fromPercentEncoding(name.toUtf8()));
|
||||
item->setData(name);
|
||||
|
||||
list << item
|
||||
<< new QStandardItem(GroupByToString(g.first))
|
||||
<< new QStandardItem(GroupByToString(g.second))
|
||||
<< new QStandardItem(GroupByToString(g.third));
|
||||
@@ -185,8 +192,9 @@ void SavedGroupingManager::UpdateModel() {
|
||||
else {
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
if (saved.at(i) == "version"_L1) continue;
|
||||
s.remove(saved.at(i));
|
||||
const QString &name = saved.at(i);
|
||||
if (name == "version"_L1) continue;
|
||||
s.remove(name);
|
||||
}
|
||||
}
|
||||
s.endGroup();
|
||||
@@ -202,7 +210,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)->text());
|
||||
s.remove(model_->item(idx.row(), 0)->data().toString());
|
||||
}
|
||||
}
|
||||
s.endGroup();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2024-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -24,18 +24,20 @@ 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 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 kSkipArticlesForArtists[] = "skip_articles_for_artists";
|
||||
constexpr char kSkipArticlesForAlbums[] = "skip_articles_for_albums";
|
||||
constexpr char kShowSortText[] = "show_sort_text";
|
||||
constexpr char kSettingsCacheSize[] = "cache_size";
|
||||
constexpr char kSettingsCacheSizeUnit[] = "cache_size_unit";
|
||||
constexpr char kSettingsDiskCacheEnable[] = "disk_cache_enable";
|
||||
|
||||
@@ -30,7 +30,7 @@ constexpr char kFileFilter[] =
|
||||
"*.aif *.aiff *.mka *.tta *.dsf *.dsd "
|
||||
"*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini "
|
||||
"*.ac3 *.dts "
|
||||
"*.mod *.s3m *.xm *.it"
|
||||
"*.mod *.s3m *.xm *.it "
|
||||
"*.spc *.vgm";
|
||||
|
||||
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
|
||||
|
||||
@@ -71,6 +71,14 @@ constexpr char kSettingsGroup[] = "DiscordRPC";
|
||||
|
||||
constexpr char kEnabled[] = "enabled";
|
||||
|
||||
constexpr char kStatusDisplayType[] = "StatusDisplayType";
|
||||
|
||||
enum class StatusDisplayType {
|
||||
App = 0,
|
||||
Artist,
|
||||
Song
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // NOTIFICATIONSSETTINGS_H
|
||||
|
||||
@@ -396,7 +396,7 @@ void ContextView::UpdateNoSong() {
|
||||
|
||||
void ContextView::NoSong() {
|
||||
|
||||
if (!widget_album_->isVisible()) {
|
||||
if (!widget_album_->isVisibleTo(this)) {
|
||||
widget_album_->show();
|
||||
}
|
||||
|
||||
@@ -440,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_->isVisible()) {
|
||||
if (action_show_album_->isChecked() && !widget_album_->isVisibleTo(this)) {
|
||||
widget_album_->show();
|
||||
widget_album_changed = true;
|
||||
}
|
||||
else if (!action_show_album_->isChecked() && widget_album_->isVisible()) {
|
||||
else if (!action_show_album_->isChecked() && widget_album_->isVisibleTo(this)) {
|
||||
widget_album_->hide();
|
||||
widget_album_changed = true;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
const int Database::kSchemaVersion = 20;
|
||||
const int Database::kSchemaVersion = 21;
|
||||
|
||||
namespace {
|
||||
constexpr char kDatabaseFilename[] = "strawberry.db";
|
||||
@@ -414,11 +414,6 @@ 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);
|
||||
|
||||
@@ -157,12 +157,16 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,10 +156,8 @@
|
||||
#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 "device/deviceview.h"
|
||||
#include "device/deviceviewcontainer.h"
|
||||
#include "transcoder/transcodedialog.h"
|
||||
#include "settings/settingsdialog.h"
|
||||
#include "constants/behavioursettings.h"
|
||||
@@ -175,6 +173,7 @@
|
||||
# include "constants/tidalsettings.h"
|
||||
#endif
|
||||
#ifdef HAVE_SPOTIFY
|
||||
# include "spotify/spotifyservice.h"
|
||||
# include "constants/spotifysettings.h"
|
||||
#endif
|
||||
#ifdef HAVE_QOBUZ
|
||||
@@ -280,7 +279,7 @@ constexpr char QTSPARKLE_URL[] = "https://www.strawberrymusicplayer.org/sparkle-
|
||||
#endif // HAVE_QTSPARKLE
|
||||
|
||||
MainWindow::MainWindow(Application *app,
|
||||
SharedPtr<SystemTrayIcon> tray_icon, OSDBase *osd,
|
||||
SharedPtr<SystemTrayIcon> systemtrayicon, OSDBase *osd,
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence,
|
||||
#endif
|
||||
@@ -292,7 +291,7 @@ MainWindow::MainWindow(Application *app,
|
||||
thumbbar_(new Windows7ThumbBar(this)),
|
||||
#endif
|
||||
app_(app),
|
||||
tray_icon_(tray_icon),
|
||||
systemtrayicon_(systemtrayicon),
|
||||
osd_(osd),
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord_rich_presence_(discord_rich_presence),
|
||||
@@ -310,9 +309,7 @@ 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)),
|
||||
@@ -375,9 +372,7 @@ 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),
|
||||
@@ -409,7 +404,11 @@ MainWindow::MainWindow(Application *app,
|
||||
// Initialize the UI
|
||||
ui_->setupUi(this);
|
||||
|
||||
setWindowIcon(IconLoader::Load(u"strawberry"_s));
|
||||
if (QGuiApplication::platformName() != "wayland"_L1) {
|
||||
setWindowIcon(IconLoader::Load(u"strawberry"_s));
|
||||
}
|
||||
|
||||
systemtrayicon_->SetDevicePixelRatioF(devicePixelRatioF());
|
||||
|
||||
QObject::connect(&*app->database(), &Database::Error, this, &MainWindow::ShowErrorDialog);
|
||||
|
||||
@@ -430,9 +429,7 @@ 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
|
||||
@@ -480,9 +477,7 @@ 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());
|
||||
@@ -554,9 +549,7 @@ 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
|
||||
@@ -718,10 +711,8 @@ 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);
|
||||
@@ -784,6 +775,9 @@ 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);
|
||||
@@ -824,9 +818,7 @@ 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();
|
||||
@@ -845,10 +837,8 @@ 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);
|
||||
@@ -858,14 +848,14 @@ MainWindow::MainWindow(Application *app,
|
||||
mac::SetApplicationHandler(this);
|
||||
#endif
|
||||
// Tray icon
|
||||
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);
|
||||
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);
|
||||
|
||||
// Windows 7 thumbbar buttons
|
||||
#ifdef Q_OS_WIN32
|
||||
@@ -980,7 +970,7 @@ 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) || defined(Q_OS_WIN32)
|
||||
#if !defined(HAVE_AUDIOCD)
|
||||
ui_->action_open_cd->setEnabled(false);
|
||||
ui_->action_open_cd->setVisible(false);
|
||||
#endif
|
||||
@@ -1043,7 +1033,7 @@ MainWindow::MainWindow(Application *app,
|
||||
show();
|
||||
break;
|
||||
case BehaviourSettings::StartupBehaviour::Hide:
|
||||
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible()) {
|
||||
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible()) {
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
@@ -1056,7 +1046,7 @@ MainWindow::MainWindow(Application *app,
|
||||
was_minimized_ = settings_.value(MainWindowSettings::kMinimized, false).toBool();
|
||||
if (was_minimized_) setWindowState(windowState() | Qt::WindowMinimized);
|
||||
|
||||
if (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
|
||||
if (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !settings_.value(MainWindowSettings::kHidden, false).toBool()) {
|
||||
show();
|
||||
}
|
||||
break;
|
||||
@@ -1168,13 +1158,13 @@ void MainWindow::ReloadSettings() {
|
||||
#ifdef Q_OS_MACOS
|
||||
constexpr bool keeprunning_available = true;
|
||||
#else
|
||||
const bool systemtray_available = tray_icon_->IsSystemTrayAvailable();
|
||||
const bool systemtray_available = systemtrayicon_->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) {
|
||||
tray_icon_->setVisible(showtrayicon);
|
||||
systemtrayicon_->setVisible(showtrayicon);
|
||||
}
|
||||
if ((!showtrayicon || !systemtray_available) && !isVisible()) {
|
||||
show();
|
||||
@@ -1199,7 +1189,7 @@ void MainWindow::ReloadSettings() {
|
||||
int iconsize = s.value(AppearanceSettings::kIconSizePlayControlButtons, 32).toInt();
|
||||
s.endGroup();
|
||||
|
||||
tray_icon_->SetTrayiconProgress(trayicon_progress);
|
||||
systemtrayicon_->SetTrayiconProgress(trayicon_progress);
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_ && !taskbar_progress) {
|
||||
@@ -1221,11 +1211,11 @@ void MainWindow::ReloadSettings() {
|
||||
ui_->volume->SetEnabled(volume_control);
|
||||
if (volume_control) {
|
||||
if (!ui_->action_mute->isVisible()) ui_->action_mute->setVisible(true);
|
||||
if (!tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(true);
|
||||
if (!systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(true);
|
||||
}
|
||||
else {
|
||||
if (ui_->action_mute->isVisible()) ui_->action_mute->setVisible(false);
|
||||
if (tray_icon_->MuteEnabled()) tray_icon_->SetMuteEnabled(false);
|
||||
if (systemtrayicon_->MuteEnabled()) systemtrayicon_->SetMuteEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1377,8 +1367,8 @@ void MainWindow::Exit() {
|
||||
if (app_->player()->GetState() == EngineBase::State::Playing) {
|
||||
app_->player()->Stop();
|
||||
hide();
|
||||
if (tray_icon_->IsSystemTrayAvailable()) {
|
||||
tray_icon_->setVisible(false);
|
||||
if (systemtrayicon_->IsSystemTrayAvailable()) {
|
||||
systemtrayicon_->setVisible(false);
|
||||
}
|
||||
return; // Don't quit the application now: wait for the fadeout finished signal
|
||||
}
|
||||
@@ -1435,7 +1425,7 @@ void MainWindow::MediaStopped() {
|
||||
|
||||
ui_->action_love->setEnabled(false);
|
||||
ui_->button_love->setEnabled(false);
|
||||
tray_icon_->LoveStateChanged(false);
|
||||
systemtrayicon_->LoveStateChanged(false);
|
||||
|
||||
if (track_position_timer_->isActive()) {
|
||||
track_position_timer_->stop();
|
||||
@@ -1444,8 +1434,8 @@ void MainWindow::MediaStopped() {
|
||||
track_slider_timer_->stop();
|
||||
}
|
||||
ui_->track_slider->SetStopped();
|
||||
tray_icon_->SetProgress(0);
|
||||
tray_icon_->SetStopped();
|
||||
systemtrayicon_->SetProgress(0);
|
||||
systemtrayicon_->SetStopped();
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1477,7 +1467,7 @@ void MainWindow::MediaPaused() {
|
||||
track_slider_timer_->start();
|
||||
}
|
||||
|
||||
tray_icon_->SetPaused();
|
||||
systemtrayicon_->SetPaused();
|
||||
|
||||
}
|
||||
|
||||
@@ -1498,7 +1488,7 @@ void MainWindow::MediaPlaying() {
|
||||
}
|
||||
ui_->action_play_pause->setEnabled(enable_play_pause);
|
||||
ui_->track_slider->SetCanSeek(can_seek);
|
||||
tray_icon_->SetPlaying(enable_play_pause);
|
||||
systemtrayicon_->SetPlaying(enable_play_pause);
|
||||
|
||||
if (!track_position_timer_->isActive()) {
|
||||
track_position_timer_->start();
|
||||
@@ -1515,18 +1505,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()->Metadata().is_metadata_good()) {
|
||||
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->Metadata());
|
||||
if (app_->scrobbler()->enabled() && playlist && playlist->current_item() && playlist->current_item()->EffectiveMetadata().is_metadata_good()) {
|
||||
app_->scrobbler()->UpdateNowPlaying(playlist->current_item()->EffectiveMetadata());
|
||||
ui_->action_love->setEnabled(true);
|
||||
ui_->button_love->setEnabled(true);
|
||||
tray_icon_->LoveStateChanged(true);
|
||||
systemtrayicon_->LoveStateChanged(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::VolumeChanged(const uint volume) {
|
||||
ui_->action_mute->setChecked(volume == 0);
|
||||
tray_icon_->MuteButtonStateChanged(volume == 0);
|
||||
systemtrayicon_->MuteButtonStateChanged(volume == 0);
|
||||
}
|
||||
|
||||
void MainWindow::SongChanged(const Song &song) {
|
||||
@@ -1536,7 +1526,7 @@ void MainWindow::SongChanged(const Song &song) {
|
||||
song_playing_ = song;
|
||||
song_ = song;
|
||||
setWindowTitle(song.PrettyTitleWithArtist());
|
||||
tray_icon_->SetProgress(0);
|
||||
systemtrayicon_->SetProgress(0);
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1562,9 +1552,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->Metadata().id() != -1) {
|
||||
if (item && item->IsLocalCollectionItem() && item->EffectiveMetadata().id() != -1) {
|
||||
|
||||
Song song = item->Metadata();
|
||||
Song song = item->EffectiveMetadata();
|
||||
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));
|
||||
@@ -1719,7 +1709,7 @@ void MainWindow::hideEvent(QHideEvent *e) {
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *e) {
|
||||
|
||||
if (!exit_ && (!tray_icon_->IsSystemTrayAvailable() || !tray_icon_->isVisible() || !keep_running_)) {
|
||||
if (!exit_ && (!systemtrayicon_->IsSystemTrayAvailable() || !systemtrayicon_->isVisible() || !keep_running_)) {
|
||||
Exit();
|
||||
}
|
||||
|
||||
@@ -1730,7 +1720,7 @@ void MainWindow::closeEvent(QCloseEvent *e) {
|
||||
void MainWindow::SetHiddenInTray(const bool hidden) {
|
||||
|
||||
if (hidden && isVisible()) {
|
||||
if (tray_icon_->IsSystemTrayAvailable() && tray_icon_->isVisible() && keep_running_) {
|
||||
if (systemtrayicon_->IsSystemTrayAvailable() && systemtrayicon_->isVisible() && keep_running_) {
|
||||
close();
|
||||
}
|
||||
else {
|
||||
@@ -1758,8 +1748,8 @@ void MainWindow::FilePathChanged(const QString &path) {
|
||||
void MainWindow::Seeked(const qint64 microseconds) {
|
||||
|
||||
const qint64 position = microseconds / kUsecPerSec;
|
||||
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));
|
||||
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));
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1774,12 +1764,12 @@ void MainWindow::UpdateTrackPosition() {
|
||||
PlaylistItemPtr item(app_->player()->GetCurrentItem());
|
||||
if (!item) return;
|
||||
|
||||
const qint64 length = (item->Metadata().length_nanosec() / kNsecPerSec);
|
||||
const qint64 length = (item->EffectiveMetadata().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) tray_icon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
if (position % 10 == 0) systemtrayicon_->SetProgress(static_cast<int>(static_cast<double>(position) / static_cast<double>(length) * 100.0));
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
if (taskbar_progress_) {
|
||||
@@ -1788,12 +1778,12 @@ void MainWindow::UpdateTrackPosition() {
|
||||
#endif
|
||||
|
||||
// Send Scrobble
|
||||
if (app_->scrobbler()->enabled() && item->Metadata().is_metadata_good()) {
|
||||
if (app_->scrobbler()->enabled() && item->EffectiveMetadata().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->Metadata(), scrobble_point);
|
||||
app_->scrobbler()->Scrobble(item->EffectiveMetadata(), scrobble_point);
|
||||
playlist->set_scrobbled(true);
|
||||
}
|
||||
}
|
||||
@@ -1910,7 +1900,7 @@ void MainWindow::AddToPlaylistFromAction(QAction *action) {
|
||||
PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_index.row());
|
||||
if (!item) continue;
|
||||
items << item;
|
||||
songs << item->Metadata();
|
||||
songs << item->EffectiveMetadata();
|
||||
}
|
||||
|
||||
// We're creating a new playlist
|
||||
@@ -1989,12 +1979,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->Metadata().url().isLocalFile()) ++local_songs;
|
||||
if (item->EffectiveMetadata().url().isLocalFile()) ++local_songs;
|
||||
|
||||
if (item->Metadata().has_cue()) {
|
||||
if (item->EffectiveMetadata().has_cue()) {
|
||||
cue_selected = true;
|
||||
}
|
||||
else if (item->Metadata().IsEditable()) {
|
||||
else if (item->EffectiveMetadata().IsEditable()) {
|
||||
++editable;
|
||||
}
|
||||
|
||||
@@ -2032,9 +2022,7 @@ 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);
|
||||
|
||||
@@ -2097,7 +2085,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->Metadata().id() != -1) {
|
||||
if (item && item->IsLocalCollectionItem() && item->EffectiveMetadata().id() != -1) {
|
||||
playlist_organize_->setVisible(local_songs > 0 && editable > 0 && !cue_selected);
|
||||
playlist_show_in_collection_->setVisible(true);
|
||||
playlist_open_in_browser_->setVisible(true);
|
||||
@@ -2107,9 +2095,7 @@ 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);
|
||||
|
||||
@@ -2189,9 +2175,9 @@ void MainWindow::RescanSongs() {
|
||||
PlaylistItemPtr item(app_->playlist_manager()->current()->item_at(source_index.row()));
|
||||
if (!item) continue;
|
||||
if (item->IsLocalCollectionItem()) {
|
||||
songs << item->Metadata();
|
||||
songs << item->EffectiveMetadata();
|
||||
}
|
||||
else if (item->Metadata().source() == Song::Source::LocalFile) {
|
||||
else if (item->EffectiveMetadata().source() == Song::Source::LocalFile) {
|
||||
QPersistentModelIndex persistent_index = QPersistentModelIndex(source_index);
|
||||
app_->playlist_manager()->current()->ItemReload(persistent_index, item->OriginalMetadata(), false);
|
||||
}
|
||||
@@ -2751,7 +2737,6 @@ 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)) {
|
||||
@@ -2761,9 +2746,6 @@ 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
|
||||
|
||||
}
|
||||
|
||||
@@ -2823,7 +2805,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::Filename)).data().toString());
|
||||
urls << QUrl(source_index.sibling(source_index.row(), static_cast<int>(Playlist::Column::URL)).data().toString());
|
||||
}
|
||||
|
||||
Utilities::OpenInFileBrowser(urls);
|
||||
@@ -2839,7 +2821,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->StreamUrl();
|
||||
urls << item->EffectiveUrl();
|
||||
}
|
||||
|
||||
if (urls.count() > 0) {
|
||||
@@ -2891,8 +2873,6 @@ void MainWindow::PlaylistSkip() {
|
||||
|
||||
void MainWindow::PlaylistCopyToDevice() {
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
|
||||
SongList songs;
|
||||
|
||||
const QModelIndexList proxy_indexes = ui_->playlist->view()->selectionModel()->selectedRows();
|
||||
@@ -2917,8 +2897,6 @@ 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) {
|
||||
@@ -3283,7 +3261,7 @@ void MainWindow::LoveButtonVisibilityChanged(const bool value) {
|
||||
else
|
||||
ui_->widget_love->hide();
|
||||
|
||||
tray_icon_->LoveVisibilityChanged(value);
|
||||
systemtrayicon_->LoveVisibilityChanged(value);
|
||||
|
||||
}
|
||||
|
||||
@@ -3306,7 +3284,7 @@ void MainWindow::Love() {
|
||||
app_->scrobbler()->Love();
|
||||
ui_->button_love->setEnabled(false);
|
||||
ui_->action_love->setEnabled(false);
|
||||
tray_icon_->LoveStateChanged(false);
|
||||
systemtrayicon_->LoveStateChanged(false);
|
||||
|
||||
}
|
||||
|
||||
@@ -3321,10 +3299,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->Metadata().url().isLocalFile()) continue;
|
||||
QString filename = item->Metadata().url().toLocalFile();
|
||||
if (!item || !item->EffectiveMetadata().url().isLocalFile()) continue;
|
||||
QString filename = item->EffectiveMetadata().url().toLocalFile();
|
||||
if (files.contains(filename)) continue;
|
||||
selected_songs << item->Metadata();
|
||||
selected_songs << item->EffectiveMetadata();
|
||||
files << filename;
|
||||
if (item == app_->player()->GetCurrentItem()) is_current_item = true;
|
||||
}
|
||||
|
||||
@@ -73,9 +73,7 @@ class CollectionViewContainer;
|
||||
class CollectionFilter;
|
||||
class AlbumCoverChoiceController;
|
||||
class CommandlineOptions;
|
||||
#ifndef Q_OS_WIN32
|
||||
class DeviceViewContainer;
|
||||
#endif
|
||||
class EditTagDialog;
|
||||
class Equalizer;
|
||||
class ErrorDialog;
|
||||
@@ -113,7 +111,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
|
||||
public:
|
||||
explicit MainWindow(Application *app,
|
||||
SharedPtr<SystemTrayIcon> tray_icon,
|
||||
SharedPtr<SystemTrayIcon> systemtrayicon,
|
||||
OSDBase *osd,
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence,
|
||||
@@ -312,7 +310,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
#endif
|
||||
|
||||
Application *app_;
|
||||
SharedPtr<SystemTrayIcon> tray_icon_;
|
||||
SharedPtr<SystemTrayIcon> systemtrayicon_;
|
||||
OSDBase *osd_;
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
discord::RichPresence *discord_rich_presence_;
|
||||
@@ -327,9 +325,7 @@ 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_;
|
||||
|
||||
@@ -380,9 +376,7 @@ 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_;
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
<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">
|
||||
@@ -37,7 +33,7 @@
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="sidebar_layout">
|
||||
<layout class="QVBoxLayout" name="layout_left">
|
||||
@@ -77,7 +73,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -102,7 +98,7 @@
|
||||
<item>
|
||||
<widget class="QFrame" name="player_controls">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="layout_player_controls">
|
||||
<property name="spacing">
|
||||
@@ -167,7 +163,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="popupMode">
|
||||
<enum>QToolButton::ToolButtonPopupMode::MenuButtonPopup</enum>
|
||||
<enum>QToolButton::MenuButtonPopup</enum>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
@@ -211,7 +207,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_love">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -237,7 +233,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_buttons">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -260,10 +256,10 @@
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Expanding</enum>
|
||||
<enum>QSizePolicy::Expanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
@@ -276,7 +272,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_volume">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -292,7 +288,7 @@
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -326,7 +322,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="status_bar_line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -380,7 +376,7 @@
|
||||
<item>
|
||||
<widget class="QLabel" name="playlist_summary">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -391,7 +387,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -401,7 +397,7 @@
|
||||
<item>
|
||||
<widget class="Line" name="line_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -580,7 +576,7 @@
|
||||
<string>Ctrl+Q</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::QuitRole</enum>
|
||||
<enum>QAction::QuitRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_stop_after_this_track">
|
||||
@@ -644,7 +640,7 @@
|
||||
<string>Ctrl+P</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::PreferencesRole</enum>
|
||||
<enum>QAction::PreferencesRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_about_strawberry">
|
||||
@@ -659,7 +655,7 @@
|
||||
<string>F1</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::AboutRole</enum>
|
||||
<enum>QAction::AboutRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_shuffle">
|
||||
@@ -785,7 +781,7 @@
|
||||
<string>About &Qt</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::AboutQtRole</enum>
|
||||
<enum>QAction::AboutQtRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_mute">
|
||||
|
||||
@@ -43,29 +43,29 @@ NetworkAccessManager::NetworkAccessManager(QObject *parent)
|
||||
|
||||
}
|
||||
|
||||
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) {
|
||||
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) {
|
||||
|
||||
QByteArray user_agent;
|
||||
if (request.hasRawHeader("User-Agent")) {
|
||||
user_agent = request.header(QNetworkRequest::UserAgentHeader).toByteArray();
|
||||
if (network_request.hasRawHeader("User-Agent")) {
|
||||
user_agent = network_request.header(QNetworkRequest::UserAgentHeader).toByteArray();
|
||||
}
|
||||
else {
|
||||
user_agent = QStringLiteral("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
|
||||
}
|
||||
|
||||
QNetworkRequest new_request(request);
|
||||
new_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
new_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
|
||||
QNetworkRequest new_network_request(network_request);
|
||||
new_network_request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
new_network_request.setHeader(QNetworkRequest::UserAgentHeader, user_agent);
|
||||
|
||||
if (op == QNetworkAccessManager::PostOperation && !new_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
|
||||
new_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
|
||||
if (op == QNetworkAccessManager::PostOperation && !new_network_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
|
||||
new_network_request.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
|
||||
}
|
||||
|
||||
// Prefer the cache unless the caller has changed the setting already
|
||||
if (request.attribute(QNetworkRequest::CacheLoadControlAttribute).toInt() == QNetworkRequest::PreferNetwork) {
|
||||
new_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
|
||||
if (!network_request.attribute(QNetworkRequest::CacheLoadControlAttribute).isValid()) {
|
||||
new_network_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
|
||||
}
|
||||
|
||||
return QNetworkAccessManager::createRequest(op, new_request, outgoingData);
|
||||
return QNetworkAccessManager::createRequest(op, new_network_request, outgoing_data);
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class NetworkAccessManager : public QNetworkAccessManager {
|
||||
explicit NetworkAccessManager(QObject *parent = nullptr);
|
||||
|
||||
protected:
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override;
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &network_request, QIODevice *outgoing_data) override;
|
||||
};
|
||||
|
||||
#endif // NETWORKACCESSMANAGER_H
|
||||
|
||||
@@ -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->Url()) {
|
||||
if (result.media_url_ == current_item->OriginalUrl()) {
|
||||
is_current = true;
|
||||
}
|
||||
else if (has_next_row && next_item->Url() == result.media_url_) {
|
||||
else if (has_next_row && next_item->OriginalUrl() == 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->Metadata();
|
||||
else if (is_next) song = next_item->Metadata();
|
||||
if (is_current) song = current_item->EffectiveMetadata();
|
||||
else if (is_next) song = next_item->EffectiveMetadata();
|
||||
|
||||
bool update = false;
|
||||
|
||||
@@ -325,7 +325,7 @@ void Player::HandleLoadResult(const UrlHandler::LoadResult &result) {
|
||||
if (
|
||||
(result.stream_url_.isValid())
|
||||
&&
|
||||
(result.stream_url_ != song.url())
|
||||
(result.stream_url_ != song.effective_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->Metadata().title() << result.stream_url_ << "position" << play_offset_nanosec_;
|
||||
qLog(Debug) << "Playing song" << current_item->EffectiveMetadata().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->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());
|
||||
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());
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -504,8 +504,8 @@ bool Player::HandleStopAfter(const Playlist::AutoScroll autoscroll) {
|
||||
|
||||
void Player::TrackEnded() {
|
||||
|
||||
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->Metadata().id() != -1) {
|
||||
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->Metadata().id());
|
||||
if (current_item_ && current_item_->IsLocalCollectionItem() && current_item_->EffectiveMetadata().id() != -1) {
|
||||
playlist_manager_->collection_backend()->IncrementPlayCountAsync(current_item_->EffectiveMetadata().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_->Metadata();
|
||||
const Song &song = current_item_->EffectiveMetadata();
|
||||
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_->Metadata().IsOnSameAlbum(playlist_manager_->active()->item_at(index)->Metadata())) {
|
||||
if (current_item_ && playlist_manager_->active()->has_item_at(index) && current_item_->EffectiveMetadata().IsOnSameAlbum(playlist_manager_->active()->item_at(index)->EffectiveMetadata())) {
|
||||
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_->StreamUrl();
|
||||
const QUrl url = current_item_->EffectiveUrl();
|
||||
|
||||
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_->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());
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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->Url()) {
|
||||
Song song = item->Metadata();
|
||||
if (item && engine_metadata.media_url == item->OriginalUrl()) {
|
||||
Song song = item->EffectiveMetadata();
|
||||
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->Url()) {
|
||||
Song song = next_item->Metadata();
|
||||
if (engine_metadata.media_url == next_item->OriginalUrl()) {
|
||||
Song song = next_item->EffectiveMetadata();
|
||||
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_->Metadata(), false);
|
||||
if (current_item_) Q_EMIT ForceShowOSD(current_item_->EffectiveMetadata(), false);
|
||||
}
|
||||
|
||||
void Player::TogglePrettyOSD() {
|
||||
if (current_item_) Q_EMIT ForceShowOSD(current_item_->Metadata(), true);
|
||||
if (current_item_) Q_EMIT ForceShowOSD(current_item_->EffectiveMetadata(), 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_->Metadata().IsOnSameAlbum(next_item->Metadata())) {
|
||||
if (engine_->crossfade_same_album() || !has_next_row || !next_item || !current_item_->EffectiveMetadata().IsOnSameAlbum(next_item->EffectiveMetadata())) {
|
||||
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->StreamUrl();
|
||||
QUrl url = next_item->EffectiveUrl();
|
||||
|
||||
// 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->Metadata();
|
||||
Song song = next_item->EffectiveMetadata();
|
||||
song.set_stream_url(url);
|
||||
next_item->SetTemporaryMetadata(song);
|
||||
next_item->SetStreamMetadata(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_->Metadata().is_module_music()) {
|
||||
if (current_item_ && current_item_->EffectiveMetadata().is_module_music()) {
|
||||
return;
|
||||
}
|
||||
|
||||
engine_->StartPreloading(next_item->Url(), url, next_item->Metadata().has_cue(), next_item->effective_beginning_nanosec(), next_item->effective_end_nanosec());
|
||||
engine_->StartPreloading(next_item->OriginalUrl(), url, next_item->EffectiveMetadata().has_cue(), next_item->effective_beginning_nanosec(), next_item->effective_end_nanosec());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -68,9 +68,13 @@
|
||||
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
|
||||
@@ -78,7 +82,9 @@ 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
|
||||
@@ -126,6 +132,9 @@ 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
|
||||
@@ -261,9 +270,13 @@ 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_;
|
||||
@@ -271,7 +284,9 @@ 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_;
|
||||
@@ -316,6 +331,9 @@ 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_;
|
||||
@@ -337,12 +355,7 @@ struct Song::Private : public QSharedData {
|
||||
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.
|
||||
|
||||
QString title_sortable_;
|
||||
QString album_sortable_;
|
||||
QString artist_sortable_;
|
||||
QString albumartist_sortable_;
|
||||
|
||||
QUrl stream_url_; // Temporary stream url set by the URL handler.
|
||||
QUrl stream_url_; // Temporary stream URL set by the URL handler.
|
||||
|
||||
};
|
||||
|
||||
@@ -384,6 +397,7 @@ Song::Private::Private(const Source source)
|
||||
art_unset_(false),
|
||||
|
||||
rating_(-1),
|
||||
bpm_(-1),
|
||||
|
||||
init_from_file_(false),
|
||||
suspicious_tags_(false)
|
||||
@@ -411,9 +425,13 @@ 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_; }
|
||||
@@ -421,7 +439,9 @@ 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_; }
|
||||
@@ -468,6 +488,9 @@ 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_; }
|
||||
@@ -511,20 +534,19 @@ 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_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_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_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; }
|
||||
@@ -532,7 +554,9 @@ 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; }
|
||||
@@ -578,6 +602,9 @@ 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; }
|
||||
@@ -600,40 +627,19 @@ 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) {
|
||||
|
||||
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_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_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); }
|
||||
@@ -652,14 +658,21 @@ 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_stream_url() const { return !d->stream_url_.isEmpty() && d->stream_url_.isValid() ? d->stream_url_ : d->url_; }
|
||||
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 QString &Song::effective_albumartist() const { return d->albumartist_.isEmpty() ? d->artist_ : d->albumartist_; }
|
||||
const QString &Song::effective_albumartist_sortable() const { return d->albumartist_.isEmpty() ? d->artist_sortable_ : d->albumartist_sortable_; }
|
||||
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_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_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(); }
|
||||
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(); }
|
||||
|
||||
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; }
|
||||
@@ -782,6 +795,31 @@ 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::MPEG;
|
||||
}
|
||||
|
||||
bool Song::albumsort_supported() const {
|
||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || 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::MPEG;
|
||||
}
|
||||
|
||||
bool Song::composersort_supported() const {
|
||||
return d->filetype_ == FileType::FLAC || d->filetype_ == FileType::OggFlac || d->filetype_ == FileType::OggVorbis || 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::MPEG;
|
||||
}
|
||||
|
||||
bool Song::save_embedded_cover_supported(const FileType filetype) {
|
||||
|
||||
return filetype == FileType::FLAC ||
|
||||
@@ -794,21 +832,6 @@ bool Song::save_embedded_cover_supported(const FileType filetype) {
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
return static_cast<int>(kRowIdColumns.indexOf(field));
|
||||
@@ -923,39 +946,63 @@ 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->album_ == other.d->album_ &&
|
||||
d->artist_ == other.d->artist_ &&
|
||||
d->albumartist_ == other.d->albumartist_ &&
|
||||
d->track_ == other.d->track_ &&
|
||||
d->disc_ == other.d->disc_ &&
|
||||
d->year_ == other.d->year_ &&
|
||||
d->originalyear_ == other.d->originalyear_ &&
|
||||
d->genre_ == other.d->genre_ &&
|
||||
d->compilation_ == other.d->compilation_ &&
|
||||
d->composer_ == other.d->composer_ &&
|
||||
d->performer_ == other.d->performer_ &&
|
||||
d->grouping_ == other.d->grouping_ &&
|
||||
d->comment_ == other.d->comment_ &&
|
||||
d->lyrics_ == other.d->lyrics_ &&
|
||||
d->artist_id_ == other.d->artist_id_ &&
|
||||
d->album_id_ == other.d->album_id_ &&
|
||||
d->song_id_ == other.d->song_id_ &&
|
||||
d->beginning_ == other.d->beginning_ &&
|
||||
length_nanosec() == other.length_nanosec() &&
|
||||
d->bitrate_ == other.d->bitrate_ &&
|
||||
d->samplerate_ == other.d->samplerate_ &&
|
||||
d->bitdepth_ == other.d->bitdepth_ &&
|
||||
d->cue_path_ == other.d->cue_path_;
|
||||
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_ &&
|
||||
d->originalyear_ == other.d->originalyear_ &&
|
||||
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_ &&
|
||||
d->artist_id_ == other.d->artist_id_ &&
|
||||
d->album_id_ == other.d->album_id_ &&
|
||||
d->song_id_ == other.d->song_id_ &&
|
||||
d->beginning_ == other.d->beginning_ &&
|
||||
length_nanosec() == other.length_nanosec() &&
|
||||
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_;
|
||||
}
|
||||
|
||||
bool Song::IsPlayStatisticsEqual(const Song &other) const {
|
||||
|
||||
return d->playcount_ == other.d->playcount_ &&
|
||||
d->skipcount_ == other.d->skipcount_ &&
|
||||
d->lastplayed_ == other.d->lastplayed_;
|
||||
d->skipcount_ == other.d->skipcount_ &&
|
||||
d->lastplayed_ == other.d->lastplayed_;
|
||||
|
||||
}
|
||||
|
||||
@@ -980,42 +1027,70 @@ bool Song::IsAcoustIdEqual(const Song &other) const {
|
||||
bool Song::IsMusicBrainzEqual(const Song &other) const {
|
||||
|
||||
return d->musicbrainz_album_artist_id_ == other.d->musicbrainz_album_artist_id_ &&
|
||||
d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ &&
|
||||
d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ &&
|
||||
d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ &&
|
||||
d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ &&
|
||||
d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ &&
|
||||
d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ &&
|
||||
d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ &&
|
||||
d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ &&
|
||||
d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_;
|
||||
d->musicbrainz_artist_id_ == other.d->musicbrainz_artist_id_ &&
|
||||
d->musicbrainz_original_artist_id_ == other.d->musicbrainz_original_artist_id_ &&
|
||||
d->musicbrainz_album_id_ == other.d->musicbrainz_album_id_ &&
|
||||
d->musicbrainz_original_album_id_ == other.d->musicbrainz_original_album_id_ &&
|
||||
d->musicbrainz_recording_id_ == other.d->musicbrainz_recording_id_ &&
|
||||
d->musicbrainz_track_id_ == other.d->musicbrainz_track_id_ &&
|
||||
d->musicbrainz_disc_id_ == other.d->musicbrainz_disc_id_ &&
|
||||
d->musicbrainz_release_group_id_ == other.d->musicbrainz_release_group_id_ &&
|
||||
d->musicbrainz_work_id_ == other.d->musicbrainz_work_id_;
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsEBUR128Equal(const Song &other) const {
|
||||
|
||||
return d->ebur128_integrated_loudness_lufs_ == other.d->ebur128_integrated_loudness_lufs_ &&
|
||||
d->ebur128_loudness_range_lu_ == other.d->ebur128_loudness_range_lu_;
|
||||
d->ebur128_loudness_range_lu_ == other.d->ebur128_loudness_range_lu_;
|
||||
|
||||
}
|
||||
|
||||
bool Song::IsArtEqual(const Song &other) const {
|
||||
|
||||
return d->art_embedded_ == other.d->art_embedded_ &&
|
||||
d->art_automatic_ == other.d->art_automatic_ &&
|
||||
d->art_manual_ == other.d->art_manual_ &&
|
||||
d->art_unset_ == other.d->art_unset_;
|
||||
d->art_automatic_ == other.d->art_automatic_ &&
|
||||
d->art_manual_ == other.d->art_manual_ &&
|
||||
d->art_unset_ == other.d->art_unset_;
|
||||
|
||||
}
|
||||
|
||||
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) &&
|
||||
IsPlayStatisticsEqual(other) &&
|
||||
IsRatingEqual(other) &&
|
||||
IsAcoustIdEqual(other) &&
|
||||
IsMusicBrainzEqual(other) &&
|
||||
IsArtEqual(other);
|
||||
IsPlayStatisticsEqual(other) &&
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
@@ -1139,6 +1214,22 @@ 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) {
|
||||
@@ -1166,6 +1257,7 @@ 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");
|
||||
@@ -1198,6 +1290,7 @@ 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;
|
||||
}
|
||||
@@ -1230,12 +1323,30 @@ 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()) {
|
||||
@@ -1250,6 +1361,7 @@ bool Song::IsFileLossless() const {
|
||||
case FileType::TrueAudio:
|
||||
case FileType::PCM:
|
||||
case FileType::CDDA:
|
||||
case FileType::ALAC:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -1279,6 +1391,7 @@ 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;
|
||||
|
||||
@@ -1306,6 +1419,7 @@ 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;
|
||||
|
||||
@@ -1416,9 +1530,13 @@ 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);
|
||||
@@ -1426,7 +1544,9 @@ 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);
|
||||
@@ -1468,7 +1588,11 @@ 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);
|
||||
@@ -1734,9 +1858,13 @@ 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_);
|
||||
@@ -1744,7 +1872,9 @@ 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_);
|
||||
@@ -1792,6 +1922,9 @@ 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_);
|
||||
@@ -1819,7 +1952,7 @@ void Song::ToXesam(QVariantMap *map) const {
|
||||
using mpris::AddMetadataAsList;
|
||||
using mpris::AsMPRISDateTimeType;
|
||||
|
||||
AddMetadata(u"xesam:url"_s, effective_stream_url().toString(), map);
|
||||
AddMetadata(u"xesam:url"_s, effective_url().toString(), map);
|
||||
AddMetadata(u"xesam:title"_s, PrettyTitle(), map);
|
||||
AddMetadataAsList(u"xesam:artist"_s, artist(), map);
|
||||
AddMetadata(u"xesam:album"_s, album(), map);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -105,6 +105,7 @@ class Song {
|
||||
IT = 21,
|
||||
SPC = 22,
|
||||
VGM = 23,
|
||||
ALAC = 24, // MP4, with ALAC codec
|
||||
CDDA = 90,
|
||||
Stream = 91
|
||||
};
|
||||
@@ -149,9 +150,13 @@ 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;
|
||||
@@ -159,7 +164,9 @@ 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;
|
||||
@@ -206,6 +213,9 @@ 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;
|
||||
@@ -249,11 +259,6 @@ 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
|
||||
@@ -261,9 +266,13 @@ 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);
|
||||
@@ -271,7 +280,9 @@ 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);
|
||||
@@ -317,6 +328,9 @@ 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);
|
||||
@@ -340,12 +354,18 @@ class Song {
|
||||
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);
|
||||
@@ -364,14 +384,21 @@ 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_stream_url() const;
|
||||
const QUrl &effective_url() const;
|
||||
const QString &effective_titlesort() const;
|
||||
const QString &effective_albumartist() const;
|
||||
const QString &effective_albumartist_sortable() const;
|
||||
const QString &effective_albumartistsort() const;
|
||||
const QString &effective_artistsort() 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_albumartist() const;
|
||||
const QString &playlist_albumartist_sortable() const;
|
||||
const QString &playlist_effective_albumartist() const;
|
||||
const QString &playlist_effective_albumartistsort() const;
|
||||
|
||||
bool is_metadata_good() const;
|
||||
bool is_local_collection_song() const;
|
||||
@@ -402,6 +429,13 @@ 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(); };
|
||||
|
||||
@@ -430,6 +464,7 @@ 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;
|
||||
@@ -438,7 +473,10 @@ 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;
|
||||
@@ -448,6 +486,7 @@ 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);
|
||||
@@ -455,9 +494,12 @@ 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);
|
||||
@@ -521,9 +563,6 @@ class Song {
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
|
||||
static QString sortable(const QString &v);
|
||||
|
||||
QSharedDataPointer<Private> d;
|
||||
};
|
||||
|
||||
|
||||
@@ -178,9 +178,11 @@ 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::SongsDurationLoaded, this, &SongLoader::AudioCDTracksLoadFinishedSlot);
|
||||
QObject::connect(cdda_song_loader, &CddaSongLoader::SongsMetadataLoaded, this, &SongLoader::AudioCDTracksTagsLoaded);
|
||||
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);
|
||||
cdda_song_loader->LoadSongs();
|
||||
return Result::Success;
|
||||
#else
|
||||
@@ -192,23 +194,38 @@ SongLoader::Result SongLoader::LoadAudioCD() {
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
|
||||
void SongLoader::AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error) {
|
||||
void SongLoader::AudioCDTracksLoadErrorSlot(const QString &error) {
|
||||
|
||||
songs_ = songs;
|
||||
errors_ << error;
|
||||
Q_EMIT AudioCDTracksLoadFinished();
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::AudioCDTracksTagsLoaded(const SongList &songs) {
|
||||
void SongLoader::AudioCDTracksLoadedSlot(const SongList &songs) {
|
||||
|
||||
CddaSongLoader *cdda_song_loader = qobject_cast<CddaSongLoader*>(sender());
|
||||
cdda_song_loader->deleteLater();
|
||||
songs_ = songs;
|
||||
Q_EMIT LoadAudioCDFinished(true);
|
||||
|
||||
Q_EMIT AudioCDTracksLoaded();
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
void SongLoader::AudioCDTracksUpdatedSlot(const SongList &songs) {
|
||||
|
||||
songs_ = songs;
|
||||
|
||||
Q_EMIT AudioCDTracksUpdated();
|
||||
|
||||
}
|
||||
|
||||
void SongLoader::AudioCDLoadingFinishedSlot() {
|
||||
|
||||
CDDASongLoader *cdda_song_loader = qobject_cast<CDDASongLoader*>(sender());
|
||||
cdda_song_loader->deleteLater();
|
||||
|
||||
Q_EMIT AudioCDLoadingFinished(true);
|
||||
|
||||
}
|
||||
|
||||
#endif // HAVE_AUDIOCD
|
||||
|
||||
SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class ParserBase;
|
||||
class CueParser;
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
class CddaSongLoader;
|
||||
class CDDASongLoader;
|
||||
#endif
|
||||
|
||||
class SongLoader : public QObject {
|
||||
@@ -90,17 +90,21 @@ class SongLoader : public QObject {
|
||||
QStringList errors() { return errors_; }
|
||||
|
||||
Q_SIGNALS:
|
||||
void AudioCDTracksLoadFinished();
|
||||
void LoadAudioCDFinished(const bool success);
|
||||
void AudioCDTracksLoaded();
|
||||
void AudioCDTracksUpdated();
|
||||
void AudioCDLoadingFinished(const bool success);
|
||||
void LoadRemoteFinished();
|
||||
|
||||
private Q_SLOTS:
|
||||
void ScheduleTimeout();
|
||||
void Timeout();
|
||||
void StopTypefind();
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
void AudioCDTracksLoadFinishedSlot(const SongList &songs, const QString &error);
|
||||
void AudioCDTracksTagsLoaded(const SongList &songs);
|
||||
void AudioCDTracksLoadErrorSlot(const QString &error);
|
||||
void AudioCDTracksLoadedSlot(const SongList &songs);
|
||||
void AudioCDTracksUpdatedSlot(const SongList &songs);
|
||||
void AudioCDLoadingFinishedSlot();
|
||||
#endif // HAVE_AUDIOCD
|
||||
|
||||
private:
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
#include <QTextStream>
|
||||
#include <QFile>
|
||||
#include <QString>
|
||||
#include <QColor>
|
||||
#include <QPalette>
|
||||
#include <QColor>
|
||||
#include <QEvent>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
@@ -80,20 +80,13 @@ void StyleSheetLoader::UpdateStyleSheet(QWidget *widget, SharedPtr<StyleSheetDat
|
||||
// Replace %palette-role with actual colours
|
||||
QPalette p(widget->palette());
|
||||
|
||||
{
|
||||
QColor alt = p.color(QPalette::AlternateBase);
|
||||
QColor color_altbase = p.color(QPalette::AlternateBase);
|
||||
#ifdef Q_OS_MACOS
|
||||
if (alt.lightness() > 180) {
|
||||
alt.setAlpha(130);
|
||||
}
|
||||
else {
|
||||
alt.setAlpha(16);
|
||||
}
|
||||
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? (color_altbase.lightness() > 180 ? 130 : 16) : color_altbase.alpha());
|
||||
#else
|
||||
alt.setAlpha(130);
|
||||
color_altbase.setAlpha(color_altbase.alpha() >= 180 ? 116 : color_altbase.alpha());
|
||||
#endif
|
||||
stylesheet.replace("%palette-alternate-base"_L1, QStringLiteral("rgba(%1,%2,%3,%4)").arg(alt.red()).arg(alt.green()).arg(alt.blue()).arg(alt.alpha()));
|
||||
}
|
||||
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()));
|
||||
|
||||
ReplaceColor(&stylesheet, u"Window"_s, p, QPalette::Window);
|
||||
ReplaceColor(&stylesheet, u"Background"_s, p, QPalette::Window);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,10 +21,19 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include <cdio/types.h>
|
||||
#include <cdio/cdio.h>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QTimer>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/logging.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "cddasongloader.h"
|
||||
#include "connecteddevice.h"
|
||||
@@ -33,7 +42,9 @@
|
||||
class DeviceLister;
|
||||
class DeviceManager;
|
||||
|
||||
CddaDevice::CddaDevice(const QUrl &url,
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
CDDADevice::CDDADevice(const QUrl &url,
|
||||
DeviceLister *lister,
|
||||
const QString &unique_id,
|
||||
DeviceManager *device_manager,
|
||||
@@ -45,36 +56,86 @@ CddaDevice::CddaDevice(const QUrl &url,
|
||||
const bool first_time,
|
||||
QObject *parent)
|
||||
: ConnectedDevice(url, lister, unique_id, device_manager, task_manager, database, tagreader_client, albumcover_loader, database_id, first_time, parent),
|
||||
cdda_song_loader_(url) {
|
||||
cdda_song_loader_(url),
|
||||
cdio_(nullptr),
|
||||
timer_disc_changed_(new QTimer(this)) {
|
||||
|
||||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsLoaded, this, &CddaDevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsDurationLoaded, this, &CddaDevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsMetadataLoaded, this, &CddaDevice::SongsLoaded);
|
||||
QObject::connect(this, &CddaDevice::SongsDiscovered, collection_model_, &CollectionModel::AddReAddOrUpdate);
|
||||
timer_disc_changed_->setInterval(1s);
|
||||
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsLoaded, this, &CDDADevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::SongsUpdated, this, &CDDADevice::SongsLoaded);
|
||||
QObject::connect(&cdda_song_loader_, &CDDASongLoader::LoadingFinished, this, &CDDADevice::SongLoadingFinished);
|
||||
QObject::connect(this, &CDDADevice::SongsDiscovered, collection_model_, &CollectionModel::AddReAddOrUpdate);
|
||||
QObject::connect(timer_disc_changed_, &QTimer::timeout, this, &CDDADevice::CheckDiscChanged);
|
||||
|
||||
}
|
||||
|
||||
bool CddaDevice::Init() {
|
||||
CDDADevice::~CDDADevice() {
|
||||
|
||||
if (cdio_) {
|
||||
cdio_destroy(cdio_);
|
||||
cdio_ = nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CDDADevice::Init() {
|
||||
|
||||
if (!cdio_) {
|
||||
cdio_ = cdio_open(url_.path().toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
if (!cdio_) return false;
|
||||
}
|
||||
|
||||
LoadSongs();
|
||||
|
||||
WatchForDiscChanges(true);
|
||||
|
||||
song_count_ = 0; // Reset song count, in case it was already set
|
||||
cdda_song_loader_.LoadSongs();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void CddaDevice::Refresh() {
|
||||
void CDDADevice::WatchForDiscChanges(const bool watch) {
|
||||
|
||||
if (!cdda_song_loader_.HasChanged()) {
|
||||
return;
|
||||
if (watch && !timer_disc_changed_->isActive()) {
|
||||
timer_disc_changed_->start();
|
||||
}
|
||||
else if (!watch && timer_disc_changed_->isActive()) {
|
||||
timer_disc_changed_->stop();
|
||||
}
|
||||
Init();
|
||||
|
||||
}
|
||||
|
||||
void CddaDevice::SongsLoaded(const SongList &songs) {
|
||||
void CDDADevice::CheckDiscChanged() {
|
||||
|
||||
if (!cdio_ || cdda_song_loader_.IsActive()) return;
|
||||
|
||||
if (cdio_get_media_changed(cdio_) == 1) {
|
||||
qLog(Debug) << "CD changed, reloading songs";
|
||||
SongsLoaded();
|
||||
LoadSongs();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CDDADevice::LoadSongs() {
|
||||
|
||||
cdda_song_loader_.LoadSongs();
|
||||
WatchForDiscChanges(false);
|
||||
|
||||
}
|
||||
|
||||
void CDDADevice::SongsLoaded(const SongList &songs) {
|
||||
|
||||
collection_model_->Reset();
|
||||
Q_EMIT SongsDiscovered(songs);
|
||||
song_count_ = songs.size();
|
||||
(void)cdio_get_media_changed(cdio_);
|
||||
|
||||
}
|
||||
|
||||
void CDDADevice::SongLoadingFinished() {
|
||||
|
||||
WatchForDiscChanges(true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -24,6 +24,11 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include <cdio/types.h>
|
||||
#include <cdio/cdio.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
@@ -35,6 +40,8 @@
|
||||
#include "cddasongloader.h"
|
||||
#include "connecteddevice.h"
|
||||
|
||||
class QTimer;
|
||||
|
||||
class DeviceLister;
|
||||
class DeviceManager;
|
||||
class TaskManager;
|
||||
@@ -42,11 +49,11 @@ class Database;
|
||||
class TagReaderClient;
|
||||
class AlbumCoverLoader;
|
||||
|
||||
class CddaDevice : public ConnectedDevice {
|
||||
class CDDADevice : public ConnectedDevice {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE explicit CddaDevice(const QUrl &url,
|
||||
Q_INVOKABLE explicit CDDADevice(const QUrl &url,
|
||||
DeviceLister *lister,
|
||||
const QString &unique_id,
|
||||
DeviceManager *device_manager,
|
||||
@@ -58,21 +65,29 @@ class CddaDevice : public ConnectedDevice {
|
||||
const bool first_time,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
~CDDADevice();
|
||||
|
||||
bool Init() override;
|
||||
void Refresh() override;
|
||||
bool CopyToStorage(const CopyJob&, QString&) override { return false; }
|
||||
bool DeleteFromStorage(const MusicStorage::DeleteJob&) override { return false; }
|
||||
|
||||
static QStringList url_schemes() { return QStringList() << QStringLiteral("cdda"); }
|
||||
|
||||
void LoadSongs();
|
||||
void WatchForDiscChanges(const bool watch);
|
||||
|
||||
Q_SIGNALS:
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
|
||||
private Q_SLOTS:
|
||||
void SongsLoaded(const SongList &songs);
|
||||
void CheckDiscChanged();
|
||||
void SongsLoaded(const SongList &songs = SongList());
|
||||
void SongLoadingFinished();
|
||||
|
||||
private:
|
||||
CddaSongLoader cdda_song_loader_;
|
||||
CDDASongLoader cdda_song_loader_;
|
||||
CdIo_t *cdio_;
|
||||
QTimer *timer_disc_changed_;
|
||||
};
|
||||
|
||||
#endif // CDDADEVICE_H
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
QStringList CddaLister::DeviceUniqueIDs() { return devices_list_; }
|
||||
QStringList CDDALister::DeviceUniqueIDs() { return devices_list_; }
|
||||
|
||||
QVariantList CddaLister::DeviceIcons(const QString &id) {
|
||||
QVariantList CDDALister::DeviceIcons(const QString &id) {
|
||||
|
||||
Q_UNUSED(id)
|
||||
|
||||
@@ -52,7 +52,7 @@ QVariantList CddaLister::DeviceIcons(const QString &id) {
|
||||
|
||||
}
|
||||
|
||||
QString CddaLister::DeviceManufacturer(const QString &id) {
|
||||
QString CDDALister::DeviceManufacturer(const QString &id) {
|
||||
|
||||
CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
cdio_hwinfo_t cd_info;
|
||||
@@ -65,7 +65,7 @@ QString CddaLister::DeviceManufacturer(const QString &id) {
|
||||
|
||||
}
|
||||
|
||||
QString CddaLister::DeviceModel(const QString &id) {
|
||||
QString CDDALister::DeviceModel(const QString &id) {
|
||||
|
||||
CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
cdio_hwinfo_t cd_info;
|
||||
@@ -78,7 +78,7 @@ QString CddaLister::DeviceModel(const QString &id) {
|
||||
|
||||
}
|
||||
|
||||
quint64 CddaLister::DeviceCapacity(const QString &id) {
|
||||
quint64 CDDALister::DeviceCapacity(const QString &id) {
|
||||
|
||||
Q_UNUSED(id)
|
||||
|
||||
@@ -86,7 +86,7 @@ quint64 CddaLister::DeviceCapacity(const QString &id) {
|
||||
|
||||
}
|
||||
|
||||
quint64 CddaLister::DeviceFreeSpace(const QString &id) {
|
||||
quint64 CDDALister::DeviceFreeSpace(const QString &id) {
|
||||
|
||||
Q_UNUSED(id)
|
||||
|
||||
@@ -94,37 +94,38 @@ quint64 CddaLister::DeviceFreeSpace(const QString &id) {
|
||||
|
||||
}
|
||||
|
||||
QVariantMap CddaLister::DeviceHardwareInfo(const QString &id) {
|
||||
QVariantMap CDDALister::DeviceHardwareInfo(const QString &id) {
|
||||
Q_UNUSED(id)
|
||||
return QVariantMap();
|
||||
}
|
||||
|
||||
QString CddaLister::MakeFriendlyName(const QString &id) {
|
||||
QString CDDALister::MakeFriendlyName(const QString &id) {
|
||||
|
||||
CdIo_t *cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
cdio_hwinfo_t cd_info;
|
||||
if (cdio_get_hwinfo(cdio, &cd_info)) {
|
||||
const QString friendly_name = QString::fromUtf8(cd_info.psz_model).trimmed();
|
||||
cdio_destroy(cdio);
|
||||
return QString::fromUtf8(cd_info.psz_model);
|
||||
return friendly_name;
|
||||
}
|
||||
cdio_destroy(cdio);
|
||||
return u"CD ("_s + id + QLatin1Char(')');
|
||||
|
||||
}
|
||||
|
||||
QList<QUrl> CddaLister::MakeDeviceUrls(const QString &id) {
|
||||
QList<QUrl> CDDALister::MakeDeviceUrls(const QString &id) {
|
||||
return QList<QUrl>() << QUrl(u"cdda://"_s + id);
|
||||
}
|
||||
|
||||
void CddaLister::UnmountDevice(const QString &id) {
|
||||
void CDDALister::UnmountDevice(const QString &id) {
|
||||
cdio_eject_media_drive(id.toLocal8Bit().constData());
|
||||
}
|
||||
|
||||
void CddaLister::UpdateDeviceFreeSpace(const QString &id) {
|
||||
void CDDALister::UpdateDeviceFreeSpace(const QString &id) {
|
||||
Q_UNUSED(id)
|
||||
}
|
||||
|
||||
bool CddaLister::Init() {
|
||||
bool CDDALister::Init() {
|
||||
|
||||
cdio_init();
|
||||
#ifdef Q_OS_MACOS
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
|
||||
#include "devicelister.h"
|
||||
|
||||
class CddaLister : public DeviceLister {
|
||||
class CDDALister : public DeviceLister {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CddaLister(QObject *parent = nullptr) : DeviceLister(parent) {}
|
||||
explicit CDDALister(QObject *parent = nullptr) : DeviceLister(parent) {}
|
||||
|
||||
QStringList DeviceUniqueIDs() override;
|
||||
QVariantList DeviceIcons(const QString &id) override;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,25 +21,24 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
|
||||
#include <glib.h>
|
||||
#include <glib/gtypes.h>
|
||||
#include <glib-object.h>
|
||||
|
||||
#include <cdio/cdio.h>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/tag/tag.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "cddasongloader.h"
|
||||
#include "includes/shared_ptr.h"
|
||||
@@ -51,18 +50,21 @@ using std::make_shared;
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
CddaSongLoader::CddaSongLoader(const QUrl &url, QObject *parent)
|
||||
CDDASongLoader::CDDASongLoader(const QUrl &url, QObject *parent)
|
||||
: QObject(parent),
|
||||
url_(url),
|
||||
network_(make_shared<NetworkAccessManager>()),
|
||||
cdda_(nullptr),
|
||||
cdio_(nullptr) {}
|
||||
network_(make_shared<NetworkAccessManager>()) {
|
||||
|
||||
CddaSongLoader::~CddaSongLoader() {
|
||||
if (cdio_) cdio_destroy(cdio_);
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
QObject::connect(this, &CDDASongLoader::LoadTagsFromMusicBrainz, this, &CDDASongLoader::LoadTagsFromMusicBrainzSlot);
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
}
|
||||
|
||||
QUrl CddaSongLoader::GetUrlFromTrack(int track_number) const {
|
||||
CDDASongLoader::~CDDASongLoader() {
|
||||
loading_future_.waitForFinished();
|
||||
}
|
||||
|
||||
QUrl CDDASongLoader::GetUrlFromTrack(int track_number) const {
|
||||
|
||||
if (url_.isEmpty()) {
|
||||
return QUrl(QStringLiteral("cdda://%1a").arg(track_number));
|
||||
@@ -72,72 +74,77 @@ QUrl CddaSongLoader::GetUrlFromTrack(int track_number) const {
|
||||
|
||||
}
|
||||
|
||||
void CddaSongLoader::LoadSongs() {
|
||||
void CDDASongLoader::LoadSongs() {
|
||||
|
||||
QMutexLocker locker(&mutex_load_);
|
||||
cdio_ = cdio_open(url_.path().toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
if (cdio_ == nullptr) {
|
||||
Error(u"Unable to open CDIO device."_s);
|
||||
if (IsActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create gstreamer cdda element
|
||||
loading_future_ = QtConcurrent::run(&CDDASongLoader::LoadSongsFromCDDA, this);
|
||||
|
||||
}
|
||||
|
||||
void CDDASongLoader::LoadSongsFromCDDA() {
|
||||
|
||||
QMutexLocker l(&mutex_load_);
|
||||
|
||||
GError *error = nullptr;
|
||||
cdda_ = gst_element_make_from_uri(GST_URI_SRC, "cdda://", nullptr, &error);
|
||||
GstElement *cdda = gst_element_factory_make("cdiocddasrc", nullptr);
|
||||
if (error) {
|
||||
Error(QStringLiteral("%1: %2").arg(error->code).arg(QString::fromUtf8(error->message)));
|
||||
}
|
||||
if (!cdda_) return;
|
||||
if (!cdda) {
|
||||
Error(tr("Could not create cdiocddasrc"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url_.isEmpty()) {
|
||||
g_object_set(cdda_, "device", g_strdup(url_.path().toLocal8Bit().constData()), nullptr);
|
||||
g_object_set(cdda, "device", g_strdup(url_.path().toLocal8Bit().constData()), nullptr);
|
||||
}
|
||||
if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda_), "paranoia-mode")) {
|
||||
g_object_set(cdda_, "paranoia-mode", 0, nullptr);
|
||||
if (g_object_class_find_property(G_OBJECT_GET_CLASS(cdda), "paranoia-mode")) {
|
||||
g_object_set(cdda, "paranoia-mode", 0, nullptr);
|
||||
}
|
||||
|
||||
// Change the element's state to ready and paused, to be able to query it
|
||||
if (gst_element_set_state(cdda_, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) {
|
||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
cdda_ = nullptr;
|
||||
if (gst_element_set_state(cdda, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) {
|
||||
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while setting CDDA device to ready state."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (gst_element_set_state(cdda_, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
|
||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
cdda_ = nullptr;
|
||||
if (gst_element_set_state(cdda, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
|
||||
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while setting CDDA device to pause state."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get number of tracks
|
||||
GstFormat fmt = gst_format_get_by_nick("track");
|
||||
GstFormat out_fmt = fmt;
|
||||
gint64 num_tracks = 0;
|
||||
if (!gst_element_query_duration(cdda_, out_fmt, &num_tracks)) {
|
||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
cdda_ = nullptr;
|
||||
GstFormat format_track = gst_format_get_by_nick("track");
|
||||
GstFormat format_duration = format_track;
|
||||
gint64 total_tracks = 0;
|
||||
if (!gst_element_query_duration(cdda, format_duration, &total_tracks)) {
|
||||
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while querying CDDA tracks."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (out_fmt != fmt) {
|
||||
qLog(Error) << "Error while querying cdda GstElement (2).";
|
||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
cdda_ = nullptr;
|
||||
if (format_duration != format_track) {
|
||||
qLog(Error) << "Error while querying CDDA GstElement (2).";
|
||||
gst_element_set_state(cdda, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda));
|
||||
cdda = nullptr;
|
||||
Error(tr("Error while querying CDDA tracks."));
|
||||
return;
|
||||
}
|
||||
|
||||
SongList songs;
|
||||
songs.reserve(num_tracks);
|
||||
for (int track_number = 1; track_number <= num_tracks; ++track_number) {
|
||||
// Init song
|
||||
QMap<int, Song> songs;
|
||||
for (int track_number = 1; track_number <= total_tracks; ++track_number) {
|
||||
Song song(Song::Source::CDDA);
|
||||
song.set_id(track_number);
|
||||
song.set_valid(true);
|
||||
@@ -145,129 +152,269 @@ void CddaSongLoader::LoadSongs() {
|
||||
song.set_url(GetUrlFromTrack(track_number));
|
||||
song.set_title(QStringLiteral("Track %1").arg(track_number));
|
||||
song.set_track(track_number);
|
||||
songs << song;
|
||||
songs.insert(track_number, song);
|
||||
}
|
||||
Q_EMIT SongsLoaded(songs);
|
||||
|
||||
Q_EMIT SongsLoaded(songs.values());
|
||||
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
gst_tag_register_musicbrainz_tags();
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
|
||||
GstElement *pipeline = gst_pipeline_new("pipeline");
|
||||
GstElement *sink = gst_element_factory_make("fakesink", nullptr);
|
||||
gst_bin_add_many(GST_BIN(pipeline), cdda_, sink, nullptr);
|
||||
gst_element_link(cdda_, sink);
|
||||
gst_bin_add_many(GST_BIN(pipeline), cdda, sink, nullptr);
|
||||
gst_element_link(cdda, sink);
|
||||
gst_element_set_state(pipeline, GST_STATE_READY);
|
||||
gst_element_set_state(pipeline, GST_STATE_PAUSED);
|
||||
|
||||
// Get TOC and TAG messages
|
||||
GstMessage *msg = nullptr;
|
||||
GstMessage *msg_toc = nullptr;
|
||||
GstMessage *msg_tag = nullptr;
|
||||
while ((msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND, static_cast<GstMessageType>(GST_MESSAGE_TOC | GST_MESSAGE_TAG)))) {
|
||||
if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TOC) {
|
||||
if (msg_toc) gst_message_unref(msg_toc); // Shouldn't happen, but just in case
|
||||
msg_toc = msg;
|
||||
}
|
||||
else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
|
||||
if (msg_tag) gst_message_unref(msg_tag);
|
||||
msg_tag = msg;
|
||||
}
|
||||
}
|
||||
int track_artist_tags = 0;
|
||||
int track_album_tags = 0;
|
||||
int track_title_tags = 0;
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
QString musicbrainz_discid;
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
GstMessageType msg_filter = static_cast<GstMessageType>(GST_MESSAGE_TOC|GST_MESSAGE_TAG);
|
||||
while (msg_filter != 0 && (msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND * 5, msg_filter))) {
|
||||
|
||||
// Handle TOC message: get tracks duration
|
||||
if (msg_toc) {
|
||||
GstToc *toc = nullptr;
|
||||
gst_message_parse_toc(msg_toc, &toc, nullptr);
|
||||
if (toc) {
|
||||
const QScopeGuard scopeguard_msg = qScopeGuard([msg]() {
|
||||
gst_message_unref(msg);
|
||||
});
|
||||
|
||||
if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TOC) {
|
||||
GstToc *toc = nullptr;
|
||||
gst_message_parse_toc(msg, &toc, nullptr);
|
||||
const QScopeGuard scopeguard_toc = qScopeGuard([toc]() {
|
||||
gst_toc_unref(toc);
|
||||
});
|
||||
GList *entries = gst_toc_get_entries(toc);
|
||||
if (entries && static_cast<guint>(songs.size()) <= g_list_length(entries)) {
|
||||
int i = 0;
|
||||
for (GList *node = entries; node != nullptr; node = node->next) {
|
||||
GstTocEntry *entry = static_cast<GstTocEntry*>(node->data);
|
||||
qint64 duration = 0;
|
||||
int track_number = 0;
|
||||
for (GList *entry_node = entries; entry_node != nullptr; entry_node = entry_node->next) {
|
||||
++track_number;
|
||||
if (songs.contains(track_number)) {
|
||||
Song &song = songs[track_number];
|
||||
GstTocEntry *entry = static_cast<GstTocEntry*>(entry_node->data);
|
||||
gint64 start = 0, stop = 0;
|
||||
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) duration = stop - start;
|
||||
songs[i++].set_length_nanosec(duration);
|
||||
if (gst_toc_entry_get_start_stop_times(entry, &start, &stop)) {
|
||||
song.set_length_nanosec(static_cast<qint64>(stop - start));
|
||||
}
|
||||
}
|
||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^ GST_MESSAGE_TOC);
|
||||
}
|
||||
}
|
||||
gst_message_unref(msg_toc);
|
||||
}
|
||||
Q_EMIT SongsDurationLoaded(songs);
|
||||
|
||||
else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
|
||||
|
||||
GstTagList *tags = nullptr;
|
||||
gst_message_parse_tag(msg, &tags);
|
||||
const QScopeGuard scopeguard_tags = qScopeGuard([tags]() {
|
||||
gst_tag_list_free(tags);
|
||||
});
|
||||
|
||||
gint64 track_index = 0;
|
||||
gst_element_query_position(cdda, format_track, &track_index);
|
||||
|
||||
char *tag = nullptr;
|
||||
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
// Handle TAG message: generate MusicBrainz DiscId
|
||||
if (msg_tag) {
|
||||
GstTagList *tags = nullptr;
|
||||
gst_message_parse_tag(msg_tag, &tags);
|
||||
char *string_mb = nullptr;
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID, &string_mb)) {
|
||||
QString musicbrainz_discid = QString::fromUtf8(string_mb);
|
||||
qLog(Info) << "MusicBrainz discid: " << musicbrainz_discid;
|
||||
|
||||
MusicBrainzClient *musicbrainz_client = new MusicBrainzClient(network_);
|
||||
QObject::connect(musicbrainz_client, &MusicBrainzClient::DiscIdFinished, this, &CddaSongLoader::AudioCDTagsLoaded);
|
||||
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
|
||||
g_free(string_mb);
|
||||
gst_message_unref(msg_tag);
|
||||
gst_tag_list_unref(tags);
|
||||
}
|
||||
}
|
||||
if (musicbrainz_discid.isEmpty()) {
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID, &tag)) {
|
||||
musicbrainz_discid = QString::fromUtf8(tag);
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
guint track_number = 0;
|
||||
if (!gst_tag_list_get_uint(tags, GST_TAG_TRACK_NUMBER, &track_number)) {
|
||||
qLog(Error) << "Could not get track number";
|
||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!songs.contains(track_number)) {
|
||||
qLog(Error) << "Got invalid track number" << track_number;
|
||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
|
||||
continue;
|
||||
}
|
||||
|
||||
Song &song = songs[track_number];
|
||||
guint64 duration = 0;
|
||||
if (gst_tag_list_get_uint64(tags, GST_TAG_DURATION, &duration)) {
|
||||
song.set_length_nanosec(static_cast<qint64>(duration));
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_ARTIST, &tag)) {
|
||||
song.set_albumartist(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_ARTIST_SORTNAME, &tag)) {
|
||||
song.set_albumartistsort(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ARTIST, &tag)) {
|
||||
song.set_artist(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
++track_artist_tags;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ARTIST_SORTNAME, &tag)) {
|
||||
song.set_artistsort(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM, &tag)) {
|
||||
song.set_album(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
++track_album_tags;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_ALBUM_SORTNAME, &tag)) {
|
||||
song.set_albumsort(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_TITLE, &tag)) {
|
||||
song.set_title(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
++track_title_tags;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_TITLE_SORTNAME, &tag)) {
|
||||
song.set_titlesort(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_GENRE, &tag)) {
|
||||
song.set_genre(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_COMPOSER, &tag)) {
|
||||
song.set_composer(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_COMPOSER_SORTNAME, &tag)) {
|
||||
song.set_composersort(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_PERFORMER, &tag)) {
|
||||
song.set_performer(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_COMMENT, &tag)) {
|
||||
song.set_comment(QString::fromUtf8(tag));
|
||||
g_free(tag);
|
||||
tag = nullptr;
|
||||
}
|
||||
guint bitrate = 0;
|
||||
if (gst_tag_list_get_uint(tags, GST_TAG_BITRATE, &bitrate)) {
|
||||
song.set_bitrate(static_cast<int>(bitrate));
|
||||
}
|
||||
|
||||
if (track_number >= total_tracks) {
|
||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
|
||||
continue;
|
||||
}
|
||||
|
||||
const gint64 next_track_index = track_index + 1;
|
||||
if (!gst_element_seek_simple(pipeline, format_track, static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_TRICKMODE), next_track_index)) {
|
||||
qLog(Error) << "Failed to seek to next track index" << next_track_index;
|
||||
msg_filter = static_cast<GstMessageType>(static_cast<int>(msg_filter) ^GST_MESSAGE_TAG);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||||
// This will also cause cdda_ to be unref'd.
|
||||
// This will also cause cdda to be unref'd.
|
||||
gst_object_unref(pipeline);
|
||||
|
||||
if ((track_artist_tags >= total_tracks && track_album_tags >= total_tracks && track_title_tags >= total_tracks)) {
|
||||
qLog(Info) << "Songs loaded from CD-Text";
|
||||
Q_EMIT SongsUpdated(songs.values());
|
||||
Q_EMIT LoadingFinished();
|
||||
}
|
||||
else {
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
if (musicbrainz_discid.isEmpty()) {
|
||||
qLog(Info) << "CD is missing tags";
|
||||
Q_EMIT LoadingFinished();
|
||||
}
|
||||
else {
|
||||
qLog(Info) << "MusicBrainz Disc ID:" << musicbrainz_discid;
|
||||
Q_EMIT LoadTagsFromMusicBrainz(musicbrainz_discid);
|
||||
}
|
||||
#else
|
||||
Q_EMIT LoadingFinished();
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
void CddaSongLoader::AudioCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results) {
|
||||
|
||||
void CDDASongLoader::LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid) const {
|
||||
|
||||
MusicBrainzClient *musicbrainz_client = new MusicBrainzClient(network_);
|
||||
QObject::connect(musicbrainz_client, &MusicBrainzClient::DiscIdFinished, this, &CDDASongLoader::LoadTagsFromMusicBrainzFinished);
|
||||
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
|
||||
|
||||
}
|
||||
|
||||
void CDDASongLoader::LoadTagsFromMusicBrainzFinished(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results, const QString &error) {
|
||||
|
||||
MusicBrainzClient *musicbrainz_client = qobject_cast<MusicBrainzClient*>(sender());
|
||||
musicbrainz_client->deleteLater();
|
||||
if (results.empty()) return;
|
||||
|
||||
if (!error.isEmpty()) {
|
||||
Error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.empty()) {
|
||||
Q_EMIT LoadingFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
SongList songs;
|
||||
songs.reserve(results.count());
|
||||
int track_number = 1;
|
||||
for (const MusicBrainzClient::Result &ret : results) {
|
||||
int track_number = 0;
|
||||
for (const MusicBrainzClient::Result &result : results) {
|
||||
++track_number;
|
||||
Song song(Song::Source::CDDA);
|
||||
song.set_artist(artist);
|
||||
song.set_album(album);
|
||||
song.set_title(ret.title_);
|
||||
song.set_length_nanosec(ret.duration_msec_ * kNsecPerMsec);
|
||||
song.set_title(result.title_);
|
||||
song.set_length_nanosec(result.duration_msec_ * kNsecPerMsec);
|
||||
song.set_track(track_number);
|
||||
song.set_year(ret.year_);
|
||||
song.set_year(result.year_);
|
||||
song.set_id(track_number);
|
||||
song.set_filetype(Song::FileType::CDDA);
|
||||
song.set_valid(true);
|
||||
// We need to set url: that's how playlist will find the correct item to update
|
||||
song.set_url(GetUrlFromTrack(track_number++));
|
||||
// We need to set URL, that's how playlist will find the correct item to update
|
||||
song.set_url(GetUrlFromTrack(track_number));
|
||||
songs << song;
|
||||
}
|
||||
Q_EMIT SongsMetadataLoaded(songs);
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
bool CddaSongLoader::HasChanged() {
|
||||
|
||||
if (cdio_ && cdio_get_media_changed(cdio_) != 1) {
|
||||
return false;
|
||||
}
|
||||
// Check if mutex is already token (i.e. init is already taking place)
|
||||
if (!mutex_load_.tryLock()) {
|
||||
return false;
|
||||
}
|
||||
mutex_load_.unlock();
|
||||
|
||||
return true;
|
||||
Q_EMIT SongsUpdated(songs);
|
||||
Q_EMIT LoadingFinished();
|
||||
|
||||
}
|
||||
|
||||
void CddaSongLoader::Error(const QString &error) {
|
||||
#endif // HAVE_MUSICBRAINZ
|
||||
|
||||
void CDDASongLoader::Error(const QString &error) {
|
||||
|
||||
qLog(Error) << error;
|
||||
Q_EMIT SongsDurationLoaded(SongList(), error);
|
||||
|
||||
Q_EMIT LoadError(error);
|
||||
Q_EMIT LoadingFinished();
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -24,11 +24,6 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include <cdio/types.h>
|
||||
#include <cdio/cdio.h>
|
||||
|
||||
#include <gst/gstelement.h>
|
||||
#include <gst/audio/gstaudiocdsrc.h>
|
||||
|
||||
@@ -36,6 +31,7 @@
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QFuture>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/song.h"
|
||||
@@ -45,39 +41,40 @@
|
||||
|
||||
class NetworkAccessManager;
|
||||
|
||||
// This class provides a (hopefully) nice, high level interface to get CD information and load tracks
|
||||
class CddaSongLoader : public QObject {
|
||||
class CDDASongLoader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CddaSongLoader(const QUrl &url, QObject *parent = nullptr);
|
||||
~CddaSongLoader() override;
|
||||
explicit CDDASongLoader(const QUrl &url, QObject *parent = nullptr);
|
||||
~CDDASongLoader() override;
|
||||
|
||||
// Load songs. Signals declared below will be emitted anytime new information will be available.
|
||||
void LoadSongs();
|
||||
bool HasChanged();
|
||||
|
||||
bool IsActive() const { return loading_future_.isRunning(); }
|
||||
|
||||
private:
|
||||
void LoadSongsFromCDDA();
|
||||
void Error(const QString &error);
|
||||
QUrl GetUrlFromTrack(const int track_number) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void SongsLoadError(const QString &error);
|
||||
void SongsLoaded(const SongList &songs);
|
||||
void SongsDurationLoaded(const SongList &songs, const QString &error = QString());
|
||||
void SongsMetadataLoaded(const SongList &songs);
|
||||
void SongsUpdated(const SongList &songs);
|
||||
void LoadError(const QString &error);
|
||||
void LoadingFinished();
|
||||
void LoadTagsFromMusicBrainz(const QString &musicbrainz_discid);
|
||||
|
||||
private Q_SLOTS:
|
||||
#ifdef HAVE_MUSICBRAINZ
|
||||
void AudioCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results);
|
||||
void LoadTagsFromMusicBrainzSlot(const QString &musicbrainz_discid) const;
|
||||
void LoadTagsFromMusicBrainzFinished(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results, const QString &error);
|
||||
#endif
|
||||
|
||||
private:
|
||||
const QUrl url_;
|
||||
SharedPtr<NetworkAccessManager> network_;
|
||||
GstElement *cdda_;
|
||||
CdIo_t *cdio_;
|
||||
QMutex mutex_load_;
|
||||
QFuture<void> loading_future_;
|
||||
};
|
||||
|
||||
#endif // CDDASONGLOADER_H
|
||||
|
||||
@@ -67,9 +67,6 @@ class ConnectedDevice : public QObject, public virtual MusicStorage, public enab
|
||||
virtual bool IsLoading() { return false; }
|
||||
virtual void NewConnection() {}
|
||||
virtual void ConnectAsync();
|
||||
// For some devices (e.g. CD devices) we don't have callbacks to be notified when something change:
|
||||
// we can call this method to refresh device's state
|
||||
virtual void Refresh() {}
|
||||
|
||||
TranscodeMode GetTranscodeMode() const override;
|
||||
Song::FileType GetTranscodeFormat() const override;
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace {
|
||||
constexpr int kDeviceSchemaVersion = 5;
|
||||
constexpr int kDeviceSchemaVersion = 6;
|
||||
}
|
||||
|
||||
DeviceDatabaseBackend::DeviceDatabaseBackend(QObject *parent)
|
||||
|
||||
@@ -36,61 +36,75 @@
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
DeviceDatabaseBackend::Device DeviceInfo::SaveToDb() const {
|
||||
void DeviceInfo::InitFromDb(const DeviceDatabaseBackend::Device &device) {
|
||||
|
||||
DeviceDatabaseBackend::Device ret;
|
||||
ret.friendly_name_ = friendly_name_;
|
||||
ret.size_ = size_;
|
||||
ret.id_ = database_id_;
|
||||
ret.icon_name_ = icon_name_;
|
||||
ret.transcode_mode_ = transcode_mode_;
|
||||
ret.transcode_format_ = transcode_format_;
|
||||
database_id_ = device.id_;
|
||||
friendly_name_ = device.friendly_name_;
|
||||
size_ = device.size_;
|
||||
transcode_mode_ = device.transcode_mode_;
|
||||
transcode_format_ = device.transcode_format_;
|
||||
icon_name_ = device.icon_name_;
|
||||
|
||||
QStringList unique_ids;
|
||||
unique_ids.reserve(backends_.count());
|
||||
for (const Backend &backend : backends_) {
|
||||
unique_ids << backend.unique_id_;
|
||||
}
|
||||
ret.unique_id_ = unique_ids.join(u',');
|
||||
InitIcon();
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void DeviceInfo::InitFromDb(const DeviceDatabaseBackend::Device &dev) {
|
||||
|
||||
database_id_ = dev.id_;
|
||||
friendly_name_ = dev.friendly_name_;
|
||||
size_ = dev.size_;
|
||||
transcode_mode_ = dev.transcode_mode_;
|
||||
transcode_format_ = dev.transcode_format_;
|
||||
icon_name_ = dev.icon_name_;
|
||||
|
||||
const QStringList unique_ids = dev.unique_id_.split(u',');
|
||||
const QStringList unique_ids = device.unique_id_.split(u',');
|
||||
for (const QString &id : unique_ids) {
|
||||
backends_ << Backend(nullptr, id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DeviceDatabaseBackend::Device DeviceInfo::SaveToDb() const {
|
||||
|
||||
DeviceDatabaseBackend::Device device;
|
||||
device.friendly_name_ = friendly_name_;
|
||||
device.size_ = size_;
|
||||
device.id_ = database_id_;
|
||||
device.icon_name_ = icon_name_;
|
||||
device.transcode_mode_ = transcode_mode_;
|
||||
device.transcode_format_ = transcode_format_;
|
||||
|
||||
QStringList unique_ids;
|
||||
unique_ids.reserve(backends_.count());
|
||||
for (const Backend &backend : backends_) {
|
||||
unique_ids << backend.unique_id_;
|
||||
}
|
||||
device.unique_id_ = unique_ids.join(u',');
|
||||
|
||||
return device;
|
||||
|
||||
}
|
||||
|
||||
const DeviceInfo::Backend *DeviceInfo::BestBackend() const {
|
||||
|
||||
int best_priority = -1;
|
||||
const Backend *ret = nullptr;
|
||||
const Backend *backend = nullptr;
|
||||
|
||||
for (int i = 0; i < backends_.count(); ++i) {
|
||||
if (backends_[i].lister_ && backends_[i].lister_->priority() > best_priority) {
|
||||
best_priority = backends_[i].lister_->priority();
|
||||
ret = &(backends_[i]);
|
||||
backend = &(backends_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ret && !backends_.isEmpty()) return &(backends_[0]);
|
||||
return ret;
|
||||
if (!backend && !backends_.isEmpty()) return &(backends_[0]);
|
||||
return backend;
|
||||
|
||||
}
|
||||
|
||||
void DeviceInfo::SetIcon(const QVariantList &icons, const QString &name_hint) {
|
||||
void DeviceInfo::InitIcon() {
|
||||
|
||||
const QStringList icon_name_list = icon_name_.split(u',');
|
||||
QVariantList icons;
|
||||
icons.reserve(icon_name_list.count());
|
||||
for (const QString &icon_name : icon_name_list) {
|
||||
icons << icon_name;
|
||||
}
|
||||
LoadIcon(icons, friendly_name_);
|
||||
|
||||
}
|
||||
|
||||
void DeviceInfo::LoadIcon(const QVariantList &icons, const QString &name_hint) {
|
||||
|
||||
icon_name_ = "device"_L1;
|
||||
|
||||
|
||||
@@ -97,8 +97,9 @@ class DeviceInfo : public SimpleTreeItem<DeviceInfo> {
|
||||
void InitFromDb(const DeviceDatabaseBackend::Device &dev);
|
||||
DeviceDatabaseBackend::Device SaveToDb() const;
|
||||
|
||||
void InitIcon();
|
||||
// Tries to load a good icon for the device. Sets icon_name_ and icon_.
|
||||
void SetIcon(const QVariantList &icons, const QString &name_hint);
|
||||
void LoadIcon(const QVariantList &icons, const QString &name_hint);
|
||||
|
||||
// Gets the best backend available (the one with the highest priority)
|
||||
const Backend *BestBackend() const;
|
||||
|
||||
@@ -109,7 +109,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
|
||||
backend_->moveToThread(database->thread());
|
||||
backend_->Init(database);
|
||||
|
||||
QObject::connect(this, &DeviceManager::DeviceCreatedFromDB, this, &DeviceManager::AddDeviceFromDB);
|
||||
QObject::connect(this, &DeviceManager::DevicesLoaded, this, &DeviceManager::AddDevicesFromDB);
|
||||
|
||||
// This reads from the database and contents on the database mutex, which can be very slow on startup.
|
||||
(void)QtConcurrent::run(&thread_pool_, &DeviceManager::LoadAllDevices, this);
|
||||
@@ -120,7 +120,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
|
||||
|
||||
// CD devices are detected via the DiskArbitration framework instead on MacOs.
|
||||
#if defined(HAVE_AUDIOCD) && !defined(Q_OS_MACOS)
|
||||
AddLister(new CddaLister);
|
||||
AddLister(new CDDALister);
|
||||
#endif
|
||||
#ifdef HAVE_UDISKS2
|
||||
AddLister(new Udisks2Lister);
|
||||
@@ -133,7 +133,7 @@ DeviceManager::DeviceManager(const SharedPtr<TaskManager> task_manager,
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_AUDIOCD
|
||||
AddDeviceClass<CddaDevice>();
|
||||
AddDeviceClass<CDDADevice>();
|
||||
#endif
|
||||
|
||||
AddDeviceClass<FilesystemDevice>();
|
||||
@@ -167,12 +167,12 @@ void DeviceManager::Exit() {
|
||||
|
||||
void DeviceManager::CloseDevices() {
|
||||
|
||||
for (DeviceInfo *info : std::as_const(devices_)) {
|
||||
if (!info->device_) continue;
|
||||
if (wait_for_exit_.contains(&*info->device_)) continue;
|
||||
wait_for_exit_ << &*info->device_;
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::destroyed, this, &DeviceManager::DeviceDestroyed);
|
||||
info->device_->Close();
|
||||
for (DeviceInfo *device_info : std::as_const(devices_)) {
|
||||
if (!device_info->device_) continue;
|
||||
if (wait_for_exit_.contains(&*device_info->device_)) continue;
|
||||
wait_for_exit_ << &*device_info->device_;
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::destroyed, this, &DeviceManager::DeviceDestroyed);
|
||||
device_info->device_->Close();
|
||||
}
|
||||
if (wait_for_exit_.isEmpty()) CloseListers();
|
||||
|
||||
@@ -224,10 +224,10 @@ void DeviceManager::ListerClosed() {
|
||||
|
||||
void DeviceManager::DeviceDestroyed() {
|
||||
|
||||
ConnectedDevice *device = static_cast<ConnectedDevice*>(sender());
|
||||
if (!wait_for_exit_.contains(device) || !backend_) return;
|
||||
ConnectedDevice *connected_device = static_cast<ConnectedDevice*>(sender());
|
||||
if (!wait_for_exit_.contains(connected_device) || !backend_) return;
|
||||
|
||||
wait_for_exit_.removeAll(device);
|
||||
wait_for_exit_.removeAll(connected_device);
|
||||
if (wait_for_exit_.isEmpty()) CloseListers();
|
||||
|
||||
}
|
||||
@@ -237,41 +237,37 @@ void DeviceManager::LoadAllDevices() {
|
||||
Q_ASSERT(QThread::currentThread() != qApp->thread());
|
||||
|
||||
const DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices();
|
||||
for (const DeviceDatabaseBackend::Device &device : devices) {
|
||||
DeviceInfo *info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||
info->InitFromDb(device);
|
||||
Q_EMIT DeviceCreatedFromDB(info);
|
||||
}
|
||||
|
||||
Q_EMIT DevicesLoaded(devices);
|
||||
|
||||
// This is done in a concurrent thread so close the unique DB connection.
|
||||
backend_->Close();
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::AddDeviceFromDB(DeviceInfo *info) {
|
||||
void DeviceManager::AddDevicesFromDB(const DeviceDatabaseBackend::DeviceList &devices) {
|
||||
|
||||
const QStringList icon_names = info->icon_name_.split(u',');
|
||||
QVariantList icons;
|
||||
icons.reserve(icon_names.count());
|
||||
for (const QString &icon_name : icon_names) {
|
||||
icons << icon_name;
|
||||
}
|
||||
info->SetIcon(icons, info->friendly_name_);
|
||||
|
||||
DeviceInfo *existing = FindEquivalentDevice(info);
|
||||
if (existing) {
|
||||
qLog(Info) << "Found existing device: " << info->friendly_name_;
|
||||
existing->icon_name_ = info->icon_name_;
|
||||
existing->icon_ = info->icon_;
|
||||
QModelIndex idx = ItemToIndex(existing);
|
||||
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
|
||||
root_->Delete(info->row);
|
||||
}
|
||||
else {
|
||||
qLog(Info) << "Device added from database: " << info->friendly_name_;
|
||||
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
|
||||
devices_ << info;
|
||||
endInsertRows();
|
||||
for (const DeviceDatabaseBackend::Device &device : devices) {
|
||||
const QStringList unique_ids = device.unique_id_.split(u',');
|
||||
DeviceInfo *device_info = FindEquivalentDevice(unique_ids);
|
||||
if (device_info && device_info->database_id_ == -1) {
|
||||
qLog(Info) << "Database device linked to physical device:" << device.friendly_name_;
|
||||
device_info->database_id_ = device.id_;
|
||||
device_info->icon_name_ = device.icon_name_;
|
||||
device_info->InitIcon();
|
||||
const QModelIndex idx = ItemToIndex(device_info);
|
||||
if (idx.isValid()) {
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
}
|
||||
else {
|
||||
qLog(Info) << "Database device:" << device.friendly_name_;
|
||||
device_info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||
device_info->InitFromDb(device);
|
||||
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
|
||||
devices_ << device_info;
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -280,30 +276,29 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
|
||||
|
||||
if (!idx.isValid() || idx.column() != 0) return QVariant();
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return QVariant();
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return QVariant();
|
||||
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:{
|
||||
QString text;
|
||||
if (!info->friendly_name_.isEmpty()) {
|
||||
text = info->friendly_name_;
|
||||
if (!device_info->friendly_name_.isEmpty()) {
|
||||
text = device_info->friendly_name_;
|
||||
}
|
||||
else if (info->BestBackend()) {
|
||||
text = info->BestBackend()->unique_id_;
|
||||
else if (device_info->BestBackend()) {
|
||||
text = device_info->BestBackend()->unique_id_;
|
||||
}
|
||||
|
||||
if (info->size_ > 0) {
|
||||
text = text + QStringLiteral(" (%1)").arg(Utilities::PrettySize(info->size_));
|
||||
if (device_info->size_ > 0) {
|
||||
text = text + QStringLiteral(" (%1)").arg(Utilities::PrettySize(device_info->size_));
|
||||
}
|
||||
if (info->device_) info->device_->Refresh();
|
||||
return text;
|
||||
}
|
||||
|
||||
case Qt::DecorationRole:{
|
||||
QPixmap pixmap = info->icon_.pixmap(kDeviceIconSize);
|
||||
QPixmap pixmap = device_info->icon_.pixmap(kDeviceIconSize);
|
||||
|
||||
if (info->backends_.isEmpty() || !info->BestBackend() || !info->BestBackend()->lister_) {
|
||||
if (device_info->backends_.isEmpty() || !device_info->BestBackend() || !device_info->BestBackend()->lister_) {
|
||||
// Disconnected but remembered
|
||||
QPainter p(&pixmap);
|
||||
p.drawPixmap(kDeviceIconSize - kDeviceIconOverlaySize, kDeviceIconSize - kDeviceIconOverlaySize, not_connected_overlay_.pixmap(kDeviceIconOverlaySize));
|
||||
@@ -313,62 +308,62 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
|
||||
}
|
||||
|
||||
case Role_FriendlyName:
|
||||
return info->friendly_name_;
|
||||
return device_info->friendly_name_;
|
||||
|
||||
case Role_UniqueId:
|
||||
if (!info->BestBackend()) return QString();
|
||||
return info->BestBackend()->unique_id_;
|
||||
if (!device_info->BestBackend()) return QString();
|
||||
return device_info->BestBackend()->unique_id_;
|
||||
|
||||
case Role_IconName:
|
||||
return info->icon_name_;
|
||||
return device_info->icon_name_;
|
||||
|
||||
case Role_Capacity:
|
||||
case MusicStorage::Role_Capacity:
|
||||
return info->size_;
|
||||
return device_info->size_;
|
||||
|
||||
case Role_FreeSpace:
|
||||
case MusicStorage::Role_FreeSpace:
|
||||
return ((info->BestBackend() && info->BestBackend()->lister_) ? info->BestBackend()->lister_->DeviceFreeSpace(info->BestBackend()->unique_id_) : QVariant());
|
||||
return ((device_info->BestBackend() && device_info->BestBackend()->lister_) ? device_info->BestBackend()->lister_->DeviceFreeSpace(device_info->BestBackend()->unique_id_) : QVariant());
|
||||
|
||||
case Role_State:
|
||||
if (info->device_) return State_Connected;
|
||||
if (info->BestBackend() && info->BestBackend()->lister_) {
|
||||
if (info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) return State_NotMounted;
|
||||
return State_NotConnected;
|
||||
if (device_info->device_) return QVariant::fromValue(State::Connected);
|
||||
if (device_info->BestBackend() && device_info->BestBackend()->lister_) {
|
||||
if (device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) return QVariant::fromValue(State::NotMounted);
|
||||
return QVariant::fromValue(State::NotConnected);
|
||||
}
|
||||
return State_Remembered;
|
||||
return QVariant::fromValue(State::Remembered);
|
||||
|
||||
case Role_UpdatingPercentage:
|
||||
if (info->task_percentage_ == -1) return QVariant();
|
||||
return info->task_percentage_;
|
||||
if (device_info->task_percentage_ == -1) return QVariant();
|
||||
return device_info->task_percentage_;
|
||||
|
||||
case MusicStorage::Role_Storage:
|
||||
if (!info->device_ && info->database_id_ != -1) {
|
||||
const_cast<DeviceManager*>(this)->Connect(info);
|
||||
if (!device_info->device_ && device_info->database_id_ != -1) {
|
||||
const_cast<DeviceManager*>(this)->Connect(device_info);
|
||||
}
|
||||
if (!info->device_) return QVariant();
|
||||
return QVariant::fromValue<SharedPtr<MusicStorage>>(info->device_);
|
||||
if (!device_info->device_) return QVariant();
|
||||
return QVariant::fromValue<SharedPtr<MusicStorage>>(device_info->device_);
|
||||
|
||||
case MusicStorage::Role_StorageForceConnect:
|
||||
if (!info->BestBackend()) return QVariant();
|
||||
if (!info->device_) {
|
||||
if (info->database_id_ == -1 && !info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) {
|
||||
if (info->BestBackend()->lister_->AskForScan(info->BestBackend()->unique_id_)) {
|
||||
if (!device_info->BestBackend()) return QVariant();
|
||||
if (!device_info->device_) {
|
||||
if (device_info->database_id_ == -1 && !device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) {
|
||||
if (device_info->BestBackend()->lister_->AskForScan(device_info->BestBackend()->unique_id_)) {
|
||||
ScopedPtr<QMessageBox> dialog(new QMessageBox(QMessageBox::Information, tr("Connect device"), tr("This is the first time you have connected this device. Strawberry will now scan the device to find music files - this may take some time."), QMessageBox::Cancel));
|
||||
QPushButton *pushbutton = dialog->addButton(tr("Connect device"), QMessageBox::AcceptRole);
|
||||
dialog->exec();
|
||||
if (dialog->clickedButton() != pushbutton) return QVariant();
|
||||
}
|
||||
}
|
||||
const_cast<DeviceManager*>(this)->Connect(info);
|
||||
const_cast<DeviceManager*>(this)->Connect(device_info);
|
||||
}
|
||||
if (!info->device_) return QVariant();
|
||||
return QVariant::fromValue<SharedPtr<MusicStorage>>(info->device_);
|
||||
if (!device_info->device_) return QVariant();
|
||||
return QVariant::fromValue<SharedPtr<MusicStorage>>(device_info->device_);
|
||||
|
||||
case Role_MountPath:{
|
||||
if (!info->device_) return QVariant();
|
||||
if (!device_info->device_) return QVariant();
|
||||
|
||||
QString ret = info->device_->url().path();
|
||||
QString ret = device_info->device_->url().path();
|
||||
#ifdef Q_OS_WIN32
|
||||
if (ret.startsWith(u'/')) ret.remove(0, 1);
|
||||
#endif
|
||||
@@ -376,17 +371,17 @@ QVariant DeviceManager::data(const QModelIndex &idx, int role) const {
|
||||
}
|
||||
|
||||
case Role_TranscodeMode:
|
||||
return static_cast<int>(info->transcode_mode_);
|
||||
return static_cast<int>(device_info->transcode_mode_);
|
||||
|
||||
case Role_TranscodeFormat:
|
||||
return static_cast<int>(info->transcode_format_);
|
||||
return static_cast<int>(device_info->transcode_format_);
|
||||
|
||||
case Role_SongCount:
|
||||
if (!info->device_) return QVariant();
|
||||
return info->device_->song_count();
|
||||
if (!device_info->device_) return QVariant();
|
||||
return device_info->device_->song_count();
|
||||
|
||||
case Role_CopyMusic:
|
||||
if (info->BestBackend() && info->BestBackend()->lister_) return info->BestBackend()->lister_->CopyMusic();
|
||||
if (device_info->BestBackend() && device_info->BestBackend()->lister_) return device_info->BestBackend()->lister_->CopyMusic();
|
||||
else return false;
|
||||
|
||||
default:
|
||||
@@ -410,7 +405,9 @@ DeviceInfo *DeviceManager::FindDeviceById(const QString &id) const {
|
||||
|
||||
for (int i = 0; i < devices_.count(); ++i) {
|
||||
for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) {
|
||||
if (backend.unique_id_ == id) return devices_[i];
|
||||
if (backend.unique_id_ == id) {
|
||||
return devices_[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,10 +422,11 @@ DeviceInfo *DeviceManager::FindDeviceByUrl(const QList<QUrl> &urls) const {
|
||||
for (int i = 0; i < devices_.count(); ++i) {
|
||||
for (const DeviceInfo::Backend &backend : std::as_const(devices_[i]->backends_)) {
|
||||
if (!backend.lister_) continue;
|
||||
|
||||
const QList<QUrl> device_urls = backend.lister_->MakeDeviceUrls(backend.unique_id_);
|
||||
for (const QUrl &url : device_urls) {
|
||||
if (urls.contains(url)) return devices_[i];
|
||||
if (urls.contains(url)) {
|
||||
return devices_[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,12 +435,15 @@ DeviceInfo *DeviceManager::FindDeviceByUrl(const QList<QUrl> &urls) const {
|
||||
|
||||
}
|
||||
|
||||
DeviceInfo *DeviceManager::FindEquivalentDevice(DeviceInfo *info) const {
|
||||
DeviceInfo *DeviceManager::FindEquivalentDevice(const QStringList &unique_ids) const {
|
||||
|
||||
for (const DeviceInfo::Backend &backend : std::as_const(info->backends_)) {
|
||||
DeviceInfo *match = FindDeviceById(backend.unique_id_);
|
||||
if (match) return match;
|
||||
for (const QString &unique_id : unique_ids) {
|
||||
DeviceInfo *device_info_match = FindDeviceById(unique_id);
|
||||
if (device_info_match) {
|
||||
return device_info_match;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
@@ -455,42 +456,42 @@ void DeviceManager::PhysicalDeviceAdded(const QString &id) {
|
||||
qLog(Info) << "Device added:" << id << lister->DeviceUniqueIDs();
|
||||
|
||||
// Do we have this device already?
|
||||
DeviceInfo *info = FindDeviceById(id);
|
||||
if (info) {
|
||||
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) {
|
||||
if (info->backends_[backend_index].unique_id_ == id) {
|
||||
info->backends_[backend_index].lister_ = lister;
|
||||
DeviceInfo *device_info = FindDeviceById(id);
|
||||
if (device_info) {
|
||||
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
|
||||
if (device_info->backends_[backend_index].unique_id_ == id) {
|
||||
device_info->backends_[backend_index].lister_ = lister;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
else {
|
||||
// Check if we have another device with the same URL
|
||||
info = FindDeviceByUrl(lister->MakeDeviceUrls(id));
|
||||
if (info) {
|
||||
device_info = FindDeviceByUrl(lister->MakeDeviceUrls(id));
|
||||
if (device_info) {
|
||||
// Add this device's lister to the existing device
|
||||
info->backends_ << DeviceInfo::Backend(lister, id);
|
||||
device_info->backends_ << DeviceInfo::Backend(lister, id);
|
||||
|
||||
// If the user hasn't saved the device in the DB yet then overwrite the device's name and icon etc.
|
||||
if (info->database_id_ == -1 && info->BestBackend() && info->BestBackend()->lister_ == lister) {
|
||||
info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||
info->size_ = lister->DeviceCapacity(id);
|
||||
info->SetIcon(lister->DeviceIcons(id), info->friendly_name_);
|
||||
if (device_info->database_id_ == -1 && device_info->BestBackend() && device_info->BestBackend()->lister_ == lister) {
|
||||
device_info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||
device_info->size_ = lister->DeviceCapacity(id);
|
||||
device_info->LoadIcon(lister->DeviceIcons(id), device_info->friendly_name_);
|
||||
}
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (idx.isValid()) Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
else {
|
||||
// It's a completely new device
|
||||
info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||
info->backends_ << DeviceInfo::Backend(lister, id);
|
||||
info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||
info->size_ = lister->DeviceCapacity(id);
|
||||
info->SetIcon(lister->DeviceIcons(id), info->friendly_name_);
|
||||
device_info = new DeviceInfo(DeviceInfo::Type::Device, root_);
|
||||
device_info->backends_ << DeviceInfo::Backend(lister, id);
|
||||
device_info->friendly_name_ = lister->MakeFriendlyName(id);
|
||||
device_info->size_ = lister->DeviceCapacity(id);
|
||||
device_info->LoadIcon(lister->DeviceIcons(id), device_info->friendly_name_);
|
||||
beginInsertRows(ItemToIndex(root_), static_cast<int>(devices_.count()), static_cast<int>(devices_.count()));
|
||||
devices_ << info;
|
||||
devices_ << device_info;
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
@@ -503,42 +504,42 @@ void DeviceManager::PhysicalDeviceRemoved(const QString &id) {
|
||||
|
||||
qLog(Info) << "Device removed:" << id;
|
||||
|
||||
DeviceInfo *info = FindDeviceById(id);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = FindDeviceById(id);
|
||||
if (!device_info) return;
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
const QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
if (info->database_id_ != -1) {
|
||||
if (device_info->database_id_ != -1) {
|
||||
// Keep the structure around, but just "disconnect" it
|
||||
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) {
|
||||
if (info->backends_[backend_index].unique_id_ == id) {
|
||||
info->backends_[backend_index].lister_ = nullptr;
|
||||
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
|
||||
if (device_info->backends_[backend_index].unique_id_ == id) {
|
||||
device_info->backends_[backend_index].lister_ = nullptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (info->device_ && info->device_->lister() == lister) {
|
||||
info->device_->Close();
|
||||
if (device_info->device_ && device_info->device_->lister() == lister) {
|
||||
device_info->device_->Close();
|
||||
}
|
||||
|
||||
if (!info->device_) Q_EMIT DeviceDisconnected(idx);
|
||||
if (!device_info->device_) Q_EMIT DeviceDisconnected(idx);
|
||||
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
else {
|
||||
// If this was the last lister for the device then remove it from the model
|
||||
for (int backend_index = 0; backend_index < info->backends_.count(); ++backend_index) {
|
||||
if (info->backends_[backend_index].unique_id_ == id) {
|
||||
info->backends_.removeAt(backend_index);
|
||||
for (int backend_index = 0; backend_index < device_info->backends_.count(); ++backend_index) {
|
||||
if (device_info->backends_[backend_index].unique_id_ == id) {
|
||||
device_info->backends_.removeAt(backend_index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (info->backends_.isEmpty()) {
|
||||
if (device_info->backends_.isEmpty()) {
|
||||
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
|
||||
devices_.removeAll(info);
|
||||
root_->Delete(info->row);
|
||||
devices_.removeAll(device_info);
|
||||
root_->Delete(device_info->row);
|
||||
endRemoveRows();
|
||||
}
|
||||
}
|
||||
@@ -550,8 +551,8 @@ void DeviceManager::PhysicalDeviceChanged(const QString &id) {
|
||||
DeviceLister *lister = qobject_cast<DeviceLister*>(sender());
|
||||
Q_UNUSED(lister);
|
||||
|
||||
DeviceInfo *info = FindDeviceById(id);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = FindDeviceById(id);
|
||||
if (!device_info) return;
|
||||
|
||||
// TODO
|
||||
|
||||
@@ -561,40 +562,41 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(const QModelIndex &idx) {
|
||||
|
||||
SharedPtr<ConnectedDevice> ret;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return ret;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return ret;
|
||||
|
||||
return Connect(info);
|
||||
return Connect(device_info);
|
||||
|
||||
}
|
||||
|
||||
SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
|
||||
SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *device_info) {
|
||||
|
||||
SharedPtr<ConnectedDevice> ret;
|
||||
|
||||
if (!info) return ret;
|
||||
if (info->device_) { // Already connected
|
||||
return info->device_;
|
||||
if (!device_info) {
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
|
||||
if (!info->BestBackend() || !info->BestBackend()->lister_) { // Not physically connected
|
||||
return ret;
|
||||
if (device_info->device_) { // Already connected
|
||||
return device_info->device_;
|
||||
}
|
||||
|
||||
if (info->BestBackend()->lister_->DeviceNeedsMount(info->BestBackend()->unique_id_)) { // Mount the device
|
||||
info->BestBackend()->lister_->MountDeviceAsync(info->BestBackend()->unique_id_);
|
||||
return ret;
|
||||
if (!device_info->BestBackend() || !device_info->BestBackend()->lister_) { // Not physically connected
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
|
||||
bool first_time = (info->database_id_ == -1);
|
||||
if (device_info->BestBackend()->lister_->DeviceNeedsMount(device_info->BestBackend()->unique_id_)) { // Mount the device
|
||||
device_info->BestBackend()->lister_->MountDeviceAsync(device_info->BestBackend()->unique_id_);
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
|
||||
const bool first_time = device_info->database_id_ == -1;
|
||||
if (first_time) {
|
||||
// We haven't stored this device in the database before
|
||||
info->database_id_ = backend_->AddDevice(info->SaveToDb());
|
||||
device_info->database_id_ = backend_->AddDevice(device_info->SaveToDb());
|
||||
}
|
||||
|
||||
// Get the device URLs
|
||||
const QList<QUrl> urls = info->BestBackend()->lister_->MakeDeviceUrls(info->BestBackend()->unique_id_);
|
||||
if (urls.isEmpty()) return ret;
|
||||
const QList<QUrl> urls = device_info->BestBackend()->lister_->MakeDeviceUrls(device_info->BestBackend()->unique_id_);
|
||||
if (urls.isEmpty()) return SharedPtr<ConnectedDevice>();
|
||||
|
||||
// Take the first URL that we have a handler for
|
||||
QUrl device_url;
|
||||
@@ -614,7 +616,7 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
|
||||
tr("This is an MTP device, but you compiled Strawberry without libmtp support.") + u" "_s +
|
||||
tr("If you continue, this device will work slowly and songs copied to it may not work."),
|
||||
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
|
||||
return ret;
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
|
||||
if (url.scheme() == "ipod"_L1) {
|
||||
@@ -622,7 +624,7 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
|
||||
tr("This is an iPod, but you compiled Strawberry without libgpod support.") + " "_L1 +
|
||||
tr("If you continue, this device will work slowly and songs copied to it may not work."),
|
||||
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
|
||||
return ret;
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,114 +637,114 @@ SharedPtr<ConnectedDevice> DeviceManager::Connect(DeviceInfo *info) {
|
||||
}
|
||||
|
||||
Q_EMIT DeviceError(tr("This type of device is not supported: %1").arg(url_strings.join(", "_L1)));
|
||||
return ret;
|
||||
return SharedPtr<ConnectedDevice>();
|
||||
}
|
||||
|
||||
QMetaObject meta_object = device_classes_.value(device_url.scheme());
|
||||
QObject *instance = meta_object.newInstance(
|
||||
Q_ARG(QUrl, device_url),
|
||||
Q_ARG(DeviceLister*, info->BestBackend()->lister_),
|
||||
Q_ARG(QString, info->BestBackend()->unique_id_),
|
||||
Q_ARG(DeviceLister*, device_info->BestBackend()->lister_),
|
||||
Q_ARG(QString, device_info->BestBackend()->unique_id_),
|
||||
Q_ARG(DeviceManager*, this),
|
||||
Q_ARG(SharedPtr<TaskManager>, task_manager_),
|
||||
Q_ARG(SharedPtr<Database>, database_),
|
||||
Q_ARG(SharedPtr<TagReaderClient>, tagreader_client_),
|
||||
Q_ARG(SharedPtr<AlbumCoverLoader>, albumcover_loader_),
|
||||
Q_ARG(int, info->database_id_),
|
||||
Q_ARG(int, device_info->database_id_),
|
||||
Q_ARG(bool, first_time));
|
||||
|
||||
ret.reset(qobject_cast<ConnectedDevice*>(instance));
|
||||
SharedPtr<ConnectedDevice> connected_device = SharedPtr<ConnectedDevice>(qobject_cast<ConnectedDevice*>(instance));
|
||||
|
||||
if (!ret) {
|
||||
if (!connected_device) {
|
||||
qLog(Warning) << "Could not create device for" << device_url.toString();
|
||||
return ret;
|
||||
return connected_device;
|
||||
}
|
||||
|
||||
bool result = ret->Init();
|
||||
bool result = connected_device->Init();
|
||||
if (!result) {
|
||||
qLog(Warning) << "Could not connect to device" << device_url.toString();
|
||||
return ret;
|
||||
return connected_device;
|
||||
}
|
||||
info->device_ = ret;
|
||||
device_info->device_ = connected_device;
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
if (!idx.isValid()) return ret;
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return connected_device;
|
||||
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::TaskStarted, this, &DeviceManager::DeviceTaskStarted);
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::SongCountUpdated, this, &DeviceManager::DeviceSongCountUpdated);
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::DeviceConnectFinished, this, &DeviceManager::DeviceConnectFinished);
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::DeviceCloseFinished, this, &DeviceManager::DeviceCloseFinished);
|
||||
QObject::connect(&*info->device_, &ConnectedDevice::Error, this, &DeviceManager::DeviceError);
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::TaskStarted, this, &DeviceManager::DeviceTaskStarted);
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::SongCountUpdated, this, &DeviceManager::DeviceSongCountUpdated);
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::DeviceConnectFinished, this, &DeviceManager::DeviceConnectFinished);
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::DeviceCloseFinished, this, &DeviceManager::DeviceCloseFinished);
|
||||
QObject::connect(&*device_info->device_, &ConnectedDevice::Error, this, &DeviceManager::DeviceError);
|
||||
|
||||
ret->ConnectAsync();
|
||||
connected_device->ConnectAsync();
|
||||
|
||||
return ret;
|
||||
return connected_device;
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::DeviceConnectFinished(const QString &id, const bool success) {
|
||||
|
||||
DeviceInfo *info = FindDeviceById(id);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = FindDeviceById(id);
|
||||
if (!device_info) return;
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
if (success) {
|
||||
Q_EMIT DeviceConnected(idx);
|
||||
}
|
||||
else {
|
||||
info->device_->Close();
|
||||
device_info->device_->Close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::DeviceCloseFinished(const QString &id) {
|
||||
|
||||
DeviceInfo *info = FindDeviceById(id);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = FindDeviceById(id);
|
||||
if (!device_info) return;
|
||||
|
||||
info->device_.reset();
|
||||
device_info->device_.reset();
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
Q_EMIT DeviceDisconnected(idx);
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
|
||||
if (info->unmount_ && info->BestBackend() && info->BestBackend()->lister_) {
|
||||
info->BestBackend()->lister_->UnmountDeviceAsync(info->BestBackend()->unique_id_);
|
||||
if (device_info->unmount_ && device_info->BestBackend() && device_info->BestBackend()->lister_) {
|
||||
device_info->BestBackend()->lister_->UnmountDeviceAsync(device_info->BestBackend()->unique_id_);
|
||||
}
|
||||
|
||||
if (info->forget_) {
|
||||
RemoveFromDB(info, idx);
|
||||
if (device_info->forget_) {
|
||||
RemoveFromDB(device_info, idx);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DeviceInfo *DeviceManager::GetDevice(const QModelIndex &idx) const {
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
return info;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
return device_info;
|
||||
|
||||
}
|
||||
|
||||
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(const QModelIndex &idx) const {
|
||||
|
||||
SharedPtr<ConnectedDevice> ret;
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return ret;
|
||||
return info->device_;
|
||||
SharedPtr<ConnectedDevice> connected_device;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return connected_device;
|
||||
return device_info->device_;
|
||||
|
||||
}
|
||||
|
||||
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(DeviceInfo *info) const {
|
||||
SharedPtr<ConnectedDevice> DeviceManager::GetConnectedDevice(DeviceInfo *device_info) const {
|
||||
|
||||
SharedPtr<ConnectedDevice> ret;
|
||||
if (!info) return ret;
|
||||
return info->device_;
|
||||
SharedPtr<ConnectedDevice> connected_device;
|
||||
if (!device_info) return connected_device;
|
||||
return device_info->device_;
|
||||
|
||||
}
|
||||
|
||||
@@ -750,9 +752,9 @@ int DeviceManager::GetDatabaseId(const QModelIndex &idx) const {
|
||||
|
||||
if (!idx.isValid()) return -1;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return -1;
|
||||
return info->database_id_;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return -1;
|
||||
return device_info->database_id_;
|
||||
|
||||
}
|
||||
|
||||
@@ -760,17 +762,17 @@ DeviceLister *DeviceManager::GetLister(const QModelIndex &idx) const {
|
||||
|
||||
if (!idx.isValid()) return nullptr;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info || !info->BestBackend()) return nullptr;
|
||||
return info->BestBackend()->lister_;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info || !device_info->BestBackend()) return nullptr;
|
||||
return device_info->BestBackend()->lister_;
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::Disconnect(DeviceInfo *info, const QModelIndex &idx) {
|
||||
void DeviceManager::Disconnect(DeviceInfo *device_info, const QModelIndex &idx) {
|
||||
|
||||
Q_UNUSED(idx);
|
||||
|
||||
info->device_->Close();
|
||||
device_info->device_->Close();
|
||||
|
||||
}
|
||||
|
||||
@@ -778,37 +780,37 @@ void DeviceManager::Forget(const QModelIndex &idx) {
|
||||
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return;
|
||||
|
||||
if (info->database_id_ == -1) return;
|
||||
if (device_info->database_id_ == -1) return;
|
||||
|
||||
if (info->device_) {
|
||||
info->forget_ = true;
|
||||
Disconnect(info, idx);
|
||||
if (device_info->device_) {
|
||||
device_info->forget_ = true;
|
||||
Disconnect(device_info, idx);
|
||||
}
|
||||
else {
|
||||
RemoveFromDB(info, idx);
|
||||
RemoveFromDB(device_info, idx);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::RemoveFromDB(DeviceInfo *info, const QModelIndex &idx) {
|
||||
void DeviceManager::RemoveFromDB(DeviceInfo *device_info, const QModelIndex &idx) {
|
||||
|
||||
backend_->RemoveDevice(info->database_id_);
|
||||
info->database_id_ = -1;
|
||||
backend_->RemoveDevice(device_info->database_id_);
|
||||
device_info->database_id_ = -1;
|
||||
|
||||
if (!info->BestBackend() || !info->BestBackend()->lister_) { // It's not attached any more so remove it from the list
|
||||
if (!device_info->BestBackend() || !device_info->BestBackend()->lister_) { // It's not attached any more so remove it from the list
|
||||
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
|
||||
devices_.removeAll(info);
|
||||
root_->Delete(info->row);
|
||||
devices_.removeAll(device_info);
|
||||
root_->Delete(device_info->row);
|
||||
endRemoveRows();
|
||||
}
|
||||
else { // It's still attached, set the name and icon back to what they were originally
|
||||
const QString id = info->BestBackend()->unique_id_;
|
||||
const QString id = device_info->BestBackend()->unique_id_;
|
||||
|
||||
info->friendly_name_ = info->BestBackend()->lister_->MakeFriendlyName(id);
|
||||
info->SetIcon(info->BestBackend()->lister_->DeviceIcons(id), info->friendly_name_);
|
||||
device_info->friendly_name_ = device_info->BestBackend()->lister_->MakeFriendlyName(id);
|
||||
device_info->LoadIcon(device_info->BestBackend()->lister_->DeviceIcons(id), device_info->friendly_name_);
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
}
|
||||
|
||||
@@ -818,18 +820,18 @@ void DeviceManager::SetDeviceOptions(const QModelIndex &idx, const QString &frie
|
||||
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return;
|
||||
|
||||
info->friendly_name_ = friendly_name;
|
||||
info->SetIcon(QVariantList() << icon_name, friendly_name);
|
||||
info->transcode_mode_ = mode;
|
||||
info->transcode_format_ = format;
|
||||
device_info->friendly_name_ = friendly_name;
|
||||
device_info->LoadIcon(QVariantList() << icon_name, friendly_name);
|
||||
device_info->transcode_mode_ = mode;
|
||||
device_info->transcode_format_ = format;
|
||||
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
|
||||
if (info->database_id_ != -1) {
|
||||
backend_->SetDeviceOptions(info->database_id_, friendly_name, icon_name, mode, format);
|
||||
if (device_info->database_id_ != -1) {
|
||||
backend_->SetDeviceOptions(device_info->database_id_, friendly_name, icon_name, mode, format);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -840,12 +842,12 @@ void DeviceManager::DeviceTaskStarted(const int id) {
|
||||
if (!device) return;
|
||||
|
||||
for (int i = 0; i < devices_.count(); ++i) {
|
||||
DeviceInfo *info = devices_.value(i);
|
||||
if (info->device_ && &*info->device_ == device) {
|
||||
QModelIndex index = ItemToIndex(info);
|
||||
DeviceInfo *device_info = devices_.value(i);
|
||||
if (device_info->device_ && &*device_info->device_ == device) {
|
||||
QModelIndex index = ItemToIndex(device_info);
|
||||
if (!index.isValid()) continue;
|
||||
active_tasks_[id] = index;
|
||||
info->task_percentage_ = 0;
|
||||
device_info->task_percentage_ = 0;
|
||||
Q_EMIT dataChanged(index, index);
|
||||
return;
|
||||
}
|
||||
@@ -864,12 +866,12 @@ void DeviceManager::TasksChanged() {
|
||||
const QPersistentModelIndex idx = active_tasks_.value(task.id);
|
||||
if (!idx.isValid()) continue;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (task.progress_max) {
|
||||
info->task_percentage_ = static_cast<int>(static_cast<float>(task.progress) / static_cast<float>(task.progress_max) * 100);
|
||||
device_info->task_percentage_ = static_cast<int>(static_cast<float>(task.progress) / static_cast<float>(task.progress_max) * 100);
|
||||
}
|
||||
else {
|
||||
info->task_percentage_ = 0;
|
||||
device_info->task_percentage_ = 0;
|
||||
}
|
||||
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
@@ -881,10 +883,10 @@ void DeviceManager::TasksChanged() {
|
||||
|
||||
if (!idx.isValid()) continue;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) continue;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) continue;
|
||||
|
||||
info->task_percentage_ = -1;
|
||||
device_info->task_percentage_ = -1;
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
|
||||
active_tasks_.remove(active_tasks_.key(idx));
|
||||
@@ -900,17 +902,17 @@ void DeviceManager::Unmount(const QModelIndex &idx) {
|
||||
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
DeviceInfo *info = IndexToItem(idx);
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = IndexToItem(idx);
|
||||
if (!device_info) return;
|
||||
|
||||
if (info->database_id_ != -1 && !info->device_) return;
|
||||
if (device_info->database_id_ != -1 && !device_info->device_) return;
|
||||
|
||||
if (info->device_) {
|
||||
info->unmount_ = true;
|
||||
Disconnect(info, idx);
|
||||
if (device_info->device_) {
|
||||
device_info->unmount_ = true;
|
||||
Disconnect(device_info, idx);
|
||||
}
|
||||
else if (info->BestBackend() && info->BestBackend()->lister_) {
|
||||
info->BestBackend()->lister_->UnmountDeviceAsync(info->BestBackend()->unique_id_);
|
||||
else if (device_info->BestBackend() && device_info->BestBackend()->lister_) {
|
||||
device_info->BestBackend()->lister_->UnmountDeviceAsync(device_info->BestBackend()->unique_id_);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -919,13 +921,13 @@ void DeviceManager::DeviceSongCountUpdated(const int count) {
|
||||
|
||||
Q_UNUSED(count);
|
||||
|
||||
ConnectedDevice *device = qobject_cast<ConnectedDevice*>(sender());
|
||||
if (!device) return;
|
||||
ConnectedDevice *connected_device = qobject_cast<ConnectedDevice*>(sender());
|
||||
if (!connected_device) return;
|
||||
|
||||
DeviceInfo *info = FindDeviceById(device->unique_id());
|
||||
if (!info) return;
|
||||
DeviceInfo *device_info = FindDeviceById(connected_device->unique_id());
|
||||
if (!device_info) return;
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return;
|
||||
|
||||
Q_EMIT dataChanged(idx, idx);
|
||||
@@ -934,10 +936,10 @@ void DeviceManager::DeviceSongCountUpdated(const int count) {
|
||||
|
||||
QString DeviceManager::DeviceNameByID(const QString &unique_id) {
|
||||
|
||||
DeviceInfo *info = FindDeviceById(unique_id);
|
||||
if (!info) return QString();
|
||||
DeviceInfo *device_info = FindDeviceById(unique_id);
|
||||
if (!device_info) return QString();
|
||||
|
||||
QModelIndex idx = ItemToIndex(info);
|
||||
QModelIndex idx = ItemToIndex(device_info);
|
||||
if (!idx.isValid()) return QString();
|
||||
|
||||
return data(idx, DeviceManager::Role_FriendlyName).toString();
|
||||
|
||||
@@ -85,11 +85,11 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
||||
LastRole,
|
||||
};
|
||||
|
||||
enum State {
|
||||
State_Remembered,
|
||||
State_NotMounted,
|
||||
State_NotConnected,
|
||||
State_Connected,
|
||||
enum class State {
|
||||
Remembered,
|
||||
NotMounted,
|
||||
NotConnected,
|
||||
Connected,
|
||||
};
|
||||
|
||||
static const int kDeviceIconSize;
|
||||
@@ -104,17 +104,17 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
||||
DeviceLister *GetLister(const QModelIndex &idx) const;
|
||||
DeviceInfo *GetDevice(const QModelIndex &idx) const;
|
||||
SharedPtr<ConnectedDevice> GetConnectedDevice(const QModelIndex &idx) const;
|
||||
SharedPtr<ConnectedDevice> GetConnectedDevice(DeviceInfo *info) const;
|
||||
SharedPtr<ConnectedDevice> GetConnectedDevice(DeviceInfo *device_info) const;
|
||||
|
||||
DeviceInfo *FindDeviceById(const QString &id) const;
|
||||
DeviceInfo *FindDeviceByUrl(const QList<QUrl> &url) const;
|
||||
QString DeviceNameByID(const QString &unique_id);
|
||||
DeviceInfo *FindEquivalentDevice(DeviceInfo *info) const;
|
||||
DeviceInfo *FindEquivalentDevice(const QStringList &unique_ids) const;
|
||||
|
||||
// Actions on devices
|
||||
SharedPtr<ConnectedDevice> Connect(DeviceInfo *info);
|
||||
SharedPtr<ConnectedDevice> Connect(DeviceInfo *device_info);
|
||||
SharedPtr<ConnectedDevice> Connect(const QModelIndex &idx);
|
||||
void Disconnect(DeviceInfo *info, const QModelIndex &idx);
|
||||
void Disconnect(DeviceInfo *device_info, const QModelIndex &idx);
|
||||
void Forget(const QModelIndex &idx);
|
||||
void UnmountAsync(const QModelIndex &idx);
|
||||
|
||||
@@ -128,9 +128,10 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
||||
|
||||
Q_SIGNALS:
|
||||
void ExitFinished();
|
||||
void DevicesLoaded(const DeviceDatabaseBackend::DeviceList &devices);
|
||||
void DeviceConnected(const QModelIndex idx);
|
||||
void DeviceDisconnected(const QModelIndex idx);
|
||||
void DeviceCreatedFromDB(DeviceInfo *info);
|
||||
void DeviceCreatedFromDB(DeviceInfo *device_info);
|
||||
void DeviceError(const QString &error);
|
||||
|
||||
private Q_SLOTS:
|
||||
@@ -143,7 +144,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
||||
void LoadAllDevices();
|
||||
void DeviceConnectFinished(const QString &id, bool success);
|
||||
void DeviceCloseFinished(const QString &id);
|
||||
void AddDeviceFromDB(DeviceInfo *info);
|
||||
void AddDevicesFromDB(const DeviceDatabaseBackend::DeviceList &devices);
|
||||
void BackendClosed();
|
||||
void ListerClosed();
|
||||
void DeviceDestroyed();
|
||||
@@ -154,7 +155,7 @@ class DeviceManager : public SimpleTreeModel<DeviceInfo> {
|
||||
|
||||
DeviceDatabaseBackend::Device InfoToDatabaseDevice(const DeviceInfo &info) const;
|
||||
|
||||
void RemoveFromDB(DeviceInfo *info, const QModelIndex &idx);
|
||||
void RemoveFromDB(DeviceInfo *device_info, const QModelIndex &idx);
|
||||
|
||||
void CloseDevices();
|
||||
void CloseListers();
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#include "devicemanager.h"
|
||||
#include "devicestatefiltermodel.h"
|
||||
|
||||
DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::State state)
|
||||
DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, const DeviceManager::State state)
|
||||
: QSortFilterProxyModel(parent),
|
||||
state_(state) {
|
||||
|
||||
@@ -40,7 +40,7 @@ DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::S
|
||||
|
||||
bool DeviceStateFilterModel::filterAcceptsRow(const int row, const QModelIndex &parent) const {
|
||||
Q_UNUSED(parent)
|
||||
return sourceModel()->index(row, 0).data(DeviceManager::Role_State).toInt() != state_ && sourceModel()->index(row, 0).data(DeviceManager::Role_CopyMusic).toBool();
|
||||
return sourceModel()->index(row, 0).data(DeviceManager::Role_State).value<DeviceManager::State>() != state_ && sourceModel()->index(row, 0).data(DeviceManager::Role_CopyMusic).toBool();
|
||||
}
|
||||
|
||||
void DeviceStateFilterModel::ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last) {
|
||||
|
||||
@@ -37,7 +37,7 @@ class DeviceStateFilterModel : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeviceStateFilterModel(QObject *parent, DeviceManager::State state = DeviceManager::State_Remembered);
|
||||
explicit DeviceStateFilterModel(QObject *parent, const DeviceManager::State state = DeviceManager::State::Remembered);
|
||||
|
||||
void setSourceModel(QAbstractItemModel *sourceModel) override;
|
||||
|
||||
@@ -52,7 +52,7 @@ class DeviceStateFilterModel : public QSortFilterProxyModel {
|
||||
void ProxyRowCountChanged(const QModelIndex &idx, const int first, const int last);
|
||||
|
||||
private:
|
||||
DeviceManager::State state_;
|
||||
const DeviceManager::State state_;
|
||||
};
|
||||
|
||||
#endif // DEVICESTATEFILTERMODEL_H
|
||||
|
||||
@@ -128,19 +128,19 @@ void DeviceItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op
|
||||
}
|
||||
else {
|
||||
switch (state) {
|
||||
case DeviceManager::State_Remembered:
|
||||
case DeviceManager::State::Remembered:
|
||||
status_text = tr("Not connected");
|
||||
break;
|
||||
|
||||
case DeviceManager::State_NotMounted:
|
||||
case DeviceManager::State::NotMounted:
|
||||
status_text = tr("Not mounted - double click to mount");
|
||||
break;
|
||||
|
||||
case DeviceManager::State_NotConnected:
|
||||
case DeviceManager::State::NotConnected:
|
||||
status_text = tr("Double click to open");
|
||||
break;
|
||||
|
||||
case DeviceManager::State_Connected:{
|
||||
case DeviceManager::State::Connected:{
|
||||
QVariant song_count = idx.data(DeviceManager::Role_SongCount);
|
||||
if (song_count.isValid()) {
|
||||
int count = song_count.toInt();
|
||||
|
||||
@@ -409,5 +409,6 @@ bool GPodDevice::FinishDelete(bool success, QString &error_text) {
|
||||
bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
|
||||
*ret << Song::FileType::MP4;
|
||||
*ret << Song::FileType::MPEG;
|
||||
*ret << Song::FileType::ALAC;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -60,6 +60,7 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
|
||||
void UpdateDeviceFreeSpace(const QString &id);
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
struct MTPDevice {
|
||||
MTPDevice() : capacity(0), free_space(0) {}
|
||||
QString vendor;
|
||||
@@ -74,6 +75,7 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
quint64 capacity;
|
||||
quint64 free_space;
|
||||
};
|
||||
#endif // HAVE_MTP
|
||||
|
||||
void ExitAsync();
|
||||
|
||||
@@ -91,11 +93,12 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
|
||||
static void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context);
|
||||
|
||||
void FoundMTPDevice(const MTPDevice &device, const QString &serial);
|
||||
#ifdef HAVE_MTP
|
||||
void FoundMTPDevice(const MTPDevice &mtp_device, const QString &serial);
|
||||
void RemovedMTPDevice(const QString &serial);
|
||||
|
||||
quint64 GetFreeSpace(const QUrl &url);
|
||||
quint64 GetCapacity(const QUrl &url);
|
||||
#endif // HAVE_MTP
|
||||
|
||||
bool IsCDDevice(const QString &serial) const;
|
||||
|
||||
@@ -103,18 +106,23 @@ class MacOsDeviceLister : public DeviceLister {
|
||||
CFRunLoopRef run_loop_;
|
||||
|
||||
QMap<QString, QString> current_devices_;
|
||||
#ifdef HAVE_MTP
|
||||
QMap<QString, MTPDevice> mtp_devices_;
|
||||
#endif
|
||||
QSet<QString> cd_devices_;
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
QMutex libmtp_mutex_;
|
||||
|
||||
static QSet<MTPDevice> sMTPDeviceList;
|
||||
#endif
|
||||
};
|
||||
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &device);
|
||||
#ifdef HAVE_MTP
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &mtp_device);
|
||||
|
||||
inline bool operator==(const MacOsDeviceLister::MTPDevice &a, const MacOsDeviceLister::MTPDevice &b) {
|
||||
return (a.vendor_id == b.vendor_id) && (a.product_id == b.product_id);
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
#endif // MACDEVICELISTER_H
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2018-2025, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,7 +21,9 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <libmtp.h>
|
||||
#ifdef HAVE_MTP
|
||||
# include <libmtp.h>
|
||||
#endif
|
||||
|
||||
#include <AvailabilityMacros.h>
|
||||
#include <CoreFoundation/CFRunLoop.h>
|
||||
@@ -41,12 +43,15 @@
|
||||
#include <QScopeGuard>
|
||||
|
||||
#include "macosdevicelister.h"
|
||||
#include "mtpconnection.h"
|
||||
#include "includes/scoped_cftyperef.h"
|
||||
#include "includes/scoped_nsobject.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/scoped_nsautorelease_pool.h"
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
# include "mtpconnection.h"
|
||||
#endif
|
||||
|
||||
#import <AppKit/NSWorkspace.h>
|
||||
#import <Foundation/NSDictionary.h>
|
||||
#import <Foundation/NSNotification.h>
|
||||
@@ -102,11 +107,15 @@ class ScopedIOObject {
|
||||
// Libgphoto2 MTP detection code:
|
||||
// http://www.sfr-fresh.com/unix/privat/libgphoto2-2.4.10.1.tar.gz:a/libgphoto2-2.4.10.1/libgphoto2_port/usb/check-mtp-device.c
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
QSet<MacOsDeviceLister::MTPDevice> MacOsDeviceLister::sMTPDeviceList;
|
||||
#endif
|
||||
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &d) {
|
||||
return qHash(d.vendor_id) ^ qHash(d.product_id);
|
||||
#ifdef HAVE_MTP
|
||||
size_t qHash(const MacOsDeviceLister::MTPDevice &mtp_device) {
|
||||
return qHash(mtp_device.vendor_id) ^ qHash(mtp_device.product_id);
|
||||
}
|
||||
#endif
|
||||
|
||||
MacOsDeviceLister::MacOsDeviceLister(QObject *parent) : DeviceLister(parent) {}
|
||||
|
||||
@@ -116,6 +125,7 @@ bool MacOsDeviceLister::Init() {
|
||||
|
||||
ScopedNSAutoreleasePool pool;
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
// Populate MTP Device list.
|
||||
if (sMTPDeviceList.empty()) {
|
||||
LIBMTP_device_entry_t *devices = nullptr;
|
||||
@@ -126,25 +136,26 @@ bool MacOsDeviceLister::Init() {
|
||||
else {
|
||||
for (int i = 0; i < num; ++i) {
|
||||
LIBMTP_device_entry_t device = devices[i];
|
||||
MTPDevice d;
|
||||
d.vendor = QString::fromLatin1(device.vendor);
|
||||
d.vendor_id = device.vendor_id;
|
||||
d.product = QString::fromLatin1(device.product);
|
||||
d.product_id = device.product_id;
|
||||
d.quirks = device.device_flags;
|
||||
sMTPDeviceList << d;
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = QString::fromLatin1(device.vendor);
|
||||
mtp_device.vendor_id = device.vendor_id;
|
||||
mtp_device.product = QString::fromLatin1(device.product);
|
||||
mtp_device.product_id = device.product_id;
|
||||
mtp_device.quirks = device.device_flags;
|
||||
sMTPDeviceList << mtp_device;
|
||||
}
|
||||
}
|
||||
|
||||
MTPDevice d;
|
||||
d.vendor = "SanDisk"_L1;
|
||||
d.vendor_id = 0x781;
|
||||
d.product = "Sansa Clip+"_L1;
|
||||
d.product_id = 0x74d0;
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = "SanDisk"_L1;
|
||||
mtp_device.vendor_id = 0x781;
|
||||
mtp_device.product = "Sansa Clip+"_L1;
|
||||
mtp_device.product_id = 0x74d0;
|
||||
|
||||
d.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
|
||||
sMTPDeviceList << d;
|
||||
mtp_device.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
|
||||
sMTPDeviceList << mtp_device;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
run_loop_ = CFRunLoopGetCurrent();
|
||||
|
||||
@@ -240,12 +251,14 @@ CFTypeRef GetUSBRegistryEntry(io_object_t device, CFStringRef key) {
|
||||
}
|
||||
|
||||
QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) {
|
||||
|
||||
ScopedCFTypeRef<CFStringRef> registry_string(reinterpret_cast<CFStringRef>(GetUSBRegistryEntry(device, key)));
|
||||
if (registry_string) {
|
||||
return QString::fromUtf8([reinterpret_cast<NSString*>(registry_string.get()) UTF8String]);
|
||||
}
|
||||
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
|
||||
@@ -277,17 +290,13 @@ NSObject *GetPropertyForDevice(io_object_t device, CFStringRef key) {
|
||||
|
||||
int GetUSBDeviceClass(io_object_t device) {
|
||||
|
||||
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(
|
||||
device,
|
||||
kIOServicePlane,
|
||||
CFSTR(kUSBInterfaceClass),
|
||||
kCFAllocatorDefault,
|
||||
kIORegistryIterateRecursively));
|
||||
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(device, kIOServicePlane, CFSTR(kUSBInterfaceClass), kCFAllocatorDefault, kIORegistryIterateRecursively));
|
||||
NSNumber *number = reinterpret_cast<NSNumber*>(interface_class.get());
|
||||
if (number) {
|
||||
int ret = [number unsignedShortValue];
|
||||
return ret;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
@@ -322,12 +331,14 @@ QString GetSerialForDevice(io_object_t device) {
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
QString GetSerialForMTPDevice(io_object_t device) {
|
||||
|
||||
scoped_nsobject<NSString> serial(reinterpret_cast<NSString*>(GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString))));
|
||||
return "MTP/"_L1 + QString::fromUtf8([serial UTF8String]);
|
||||
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
|
||||
|
||||
@@ -343,6 +354,7 @@ QString FindDeviceProperty(const QString &bsd_name, CFStringRef property) {
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
quint64 MacOsDeviceLister::GetFreeSpace(const QUrl &url) {
|
||||
|
||||
QMutexLocker l(&libmtp_mutex_);
|
||||
@@ -380,6 +392,8 @@ quint64 MacOsDeviceLister::GetCapacity(const QUrl &url) {
|
||||
|
||||
}
|
||||
|
||||
#endif // HAVE_MTP
|
||||
|
||||
void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
|
||||
@@ -390,12 +404,12 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
NSString *kind = [properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionMediaKindKey)];
|
||||
if (kind && strcmp([kind UTF8String], kIOCDMediaClass) == 0) {
|
||||
// CD inserted.
|
||||
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
me->cd_devices_ << bsd_name;
|
||||
Q_EMIT me->DeviceAdded(bsd_name);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#endif // HAVE_AUDIOCD
|
||||
|
||||
NSURL *volume_path = [[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy];
|
||||
|
||||
@@ -403,8 +417,8 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk));
|
||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(device.get()));
|
||||
if (class_name && CFStringCompare(class_name.get(), CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) {
|
||||
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
|
||||
CFMutableDictionaryRef cf_properties;
|
||||
kern_return_t ret = IORegistryEntryCreateCFProperties(device.get(), &cf_properties, kCFAllocatorDefault, 0);
|
||||
@@ -412,7 +426,7 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
if (ret == KERN_SUCCESS) {
|
||||
scoped_nsobject<NSDictionary> dict(reinterpret_cast<NSDictionary*>(cf_properties)); // Takes ownership.
|
||||
if ([[dict objectForKey:@"Removable"] intValue] == 1) {
|
||||
QString serial = GetSerialForDevice(device.get());
|
||||
const QString serial = GetSerialForDevice(device.get());
|
||||
if (!serial.isEmpty()) {
|
||||
me->current_devices_[serial] = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
Q_EMIT me->DeviceAdded(serial);
|
||||
@@ -427,10 +441,9 @@ void MacOsDeviceLister::DiskAddedCallback(DADiskRef disk, void *context) {
|
||||
void MacOsDeviceLister::DiskRemovedCallback(DADiskRef disk, void *context) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(context);
|
||||
// We cannot access the USB tree when the disk is removed but we still get
|
||||
// the BSD disk name.
|
||||
// We cannot access the USB tree when the disk is removed but we still get the BSD disk name.
|
||||
|
||||
QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
const QString bsd_name = QString::fromLatin1(DADiskGetBSDName(disk));
|
||||
if (me->cd_devices_.remove(bsd_name)) {
|
||||
Q_EMIT me->DeviceRemoved(bsd_name);
|
||||
return;
|
||||
@@ -496,6 +509,7 @@ int GetBusNumber(io_object_t o) {
|
||||
void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
|
||||
Q_UNUSED(me)
|
||||
|
||||
io_object_t object;
|
||||
while ((object = IOIteratorNext(it))) {
|
||||
@@ -503,30 +517,34 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
|
||||
|
||||
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
||||
|
||||
const int interface_class = GetUSBDeviceClass(object);
|
||||
qLog(Debug) << "Interface class:" << interface_class;
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
|
||||
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
|
||||
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
|
||||
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
|
||||
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
|
||||
int interface_class = GetUSBDeviceClass(object);
|
||||
qLog(Debug) << "Interface class:" << interface_class;
|
||||
|
||||
QString serial = GetSerialForMTPDevice(object);
|
||||
const QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice device;
|
||||
device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
device.product = QString::fromUtf8([product UTF8String]);
|
||||
device.vendor_id = [vendor_id unsignedShortValue];
|
||||
device.product_id = [product_id unsignedShortValue];
|
||||
device.quirks = 0;
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
mtp_device.product = QString::fromUtf8([product UTF8String]);
|
||||
mtp_device.vendor_id = [vendor_id unsignedShortValue];
|
||||
mtp_device.product_id = [product_id unsignedShortValue];
|
||||
mtp_device.quirks = 0;
|
||||
|
||||
device.bus = -1;
|
||||
device.address = -1;
|
||||
mtp_device.bus = -1;
|
||||
mtp_device.address = -1;
|
||||
|
||||
if (device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products.
|
||||
if (mtp_device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products.
|
||||
// Blacklist ilok2 as this probe may be breaking it.
|
||||
(device.vendor_id == 0x088e && device.product_id == 0x5036) ||
|
||||
(mtp_device.vendor_id == 0x088e && mtp_device.product_id == 0x5036) ||
|
||||
// Blacklist eLicenser
|
||||
(device.vendor_id == 0x0819 && device.product_id == 0x0101) ||
|
||||
(mtp_device.vendor_id == 0x0819 && mtp_device.product_id == 0x0101) ||
|
||||
// Skip HID devices, printers and hubs.
|
||||
interface_class == kUSBHIDInterfaceClass ||
|
||||
interface_class == kUSBPrintingInterfaceClass ||
|
||||
@@ -535,31 +553,28 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
}
|
||||
|
||||
NSNumber *addr = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR("USB Address")));
|
||||
int bus = GetBusNumber(object);
|
||||
const int bus = GetBusNumber(object);
|
||||
if (!addr || bus == -1) {
|
||||
// Failed to get bus or address number.
|
||||
continue;
|
||||
}
|
||||
device.bus = bus;
|
||||
device.address = [addr intValue];
|
||||
mtp_device.bus = bus;
|
||||
mtp_device.address = [addr intValue];
|
||||
|
||||
// First check the libmtp device list.
|
||||
QSet<MTPDevice>::const_iterator it2 = sMTPDeviceList.find(device);
|
||||
QSet<MTPDevice>::const_iterator it2 = sMTPDeviceList.find(mtp_device);
|
||||
if (it2 != sMTPDeviceList.end()) {
|
||||
// Fill in quirks flags from libmtp.
|
||||
device.quirks = it2->quirks;
|
||||
me->FoundMTPDevice(device, GetSerialForMTPDevice(object));
|
||||
mtp_device.quirks = it2->quirks;
|
||||
me->FoundMTPDevice(mtp_device, GetSerialForMTPDevice(object));
|
||||
continue;
|
||||
}
|
||||
|
||||
#endif // HAVE_MTP
|
||||
|
||||
IOCFPlugInInterface **plugin_interface = nullptr;
|
||||
SInt32 score;
|
||||
kern_return_t err = IOCreatePlugInInterfaceForService(
|
||||
object,
|
||||
kIOUSBDeviceUserClientTypeID,
|
||||
kIOCFPlugInInterfaceID,
|
||||
&plugin_interface,
|
||||
&score);
|
||||
kern_return_t err = IOCreatePlugInInterfaceForService(object, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin_interface, &score);
|
||||
if (err != KERN_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
@@ -590,7 +605,7 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
bool ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, 2, &data);
|
||||
if (!ret) continue;
|
||||
|
||||
UInt8 string_len = data[0];
|
||||
const UInt8 string_len = data[0];
|
||||
|
||||
ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, string_len, &data);
|
||||
if (!ret) continue;
|
||||
@@ -599,6 +614,7 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
// Because this was designed by MS, the characters are in UTF-16 (LE?).
|
||||
QString str = QString::fromUtf16(reinterpret_cast<char16_t*>(data.data() + 2), (data.size() / 2) - 2);
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (str.startsWith("MSFT100"_L1)) {
|
||||
// We got the OS descriptor!
|
||||
char vendor_code = data[16];
|
||||
@@ -621,8 +637,10 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
continue;
|
||||
}
|
||||
// Hurray! We made it!
|
||||
me->FoundMTPDevice(device, serial);
|
||||
me->FoundMTPDevice(mtp_device, serial);
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,30 +649,39 @@ void MacOsDeviceLister::USBDeviceAddedCallback(void *refcon, io_iterator_t it) {
|
||||
void MacOsDeviceLister::USBDeviceRemovedCallback(void *refcon, io_iterator_t it) {
|
||||
|
||||
MacOsDeviceLister *me = reinterpret_cast<MacOsDeviceLister*>(refcon);
|
||||
Q_UNUSED(me)
|
||||
|
||||
io_object_t object;
|
||||
while ((object = IOIteratorNext(it))) {
|
||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object));
|
||||
const QScopeGuard io_object_release = qScopeGuard([object]() { IOObjectRelease(object); });
|
||||
|
||||
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
NSString *vendor = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBVendorString)));
|
||||
NSString *product = reinterpret_cast<NSString*>(GetPropertyForDevice(object, CFSTR(kUSBProductString)));
|
||||
NSNumber *vendor_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBVendorID)));
|
||||
NSNumber *product_id = reinterpret_cast<NSNumber*>(GetPropertyForDevice(object, CFSTR(kUSBProductID)));
|
||||
QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice device;
|
||||
device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
device.product = QString::fromUtf8([product UTF8String]);
|
||||
device.vendor_id = [vendor_id unsignedShortValue];
|
||||
device.product_id = [product_id unsignedShortValue];
|
||||
const QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice mtp_device;
|
||||
mtp_device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
mtp_device.product = QString::fromUtf8([product UTF8String]);
|
||||
mtp_device.vendor_id = [vendor_id unsignedShortValue];
|
||||
mtp_device.product_id = [product_id unsignedShortValue];
|
||||
|
||||
me->RemovedMTPDevice(serial);
|
||||
#endif // HAVE_MTP
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
|
||||
void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
|
||||
|
||||
int count = mtp_devices_.remove(serial);
|
||||
@@ -668,34 +695,40 @@ void MacOsDeviceLister::RemovedMTPDevice(const QString &serial) {
|
||||
void MacOsDeviceLister::FoundMTPDevice(const MTPDevice &device, const QString &serial) {
|
||||
|
||||
qLog(Debug) << "New MTP device detected!" << device.bus << device.address;
|
||||
|
||||
mtp_devices_[serial] = device;
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice *d = &mtp_devices_[serial];
|
||||
d->capacity = GetCapacity(urls[0]);
|
||||
d->free_space = GetFreeSpace(urls[0]);
|
||||
const QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice *mtp_device = &mtp_devices_[serial];
|
||||
mtp_device->capacity = GetCapacity(urls[0]);
|
||||
mtp_device->free_space = GetFreeSpace(urls[0]);
|
||||
|
||||
Q_EMIT DeviceAdded(serial);
|
||||
|
||||
}
|
||||
|
||||
bool IsMTPSerial(const QString &serial) { return serial.startsWith("MTP"_L1); }
|
||||
|
||||
#endif // HAVE_MTP
|
||||
|
||||
bool MacOsDeviceLister::IsCDDevice(const QString &serial) const {
|
||||
return cd_devices_.contains(serial);
|
||||
}
|
||||
|
||||
QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
const MTPDevice &device = mtp_devices_[serial];
|
||||
if (device.vendor.isEmpty()) {
|
||||
return device.product;
|
||||
const MTPDevice &mtp_device = mtp_devices_[serial];
|
||||
if (mtp_device.vendor.isEmpty()) {
|
||||
return mtp_device.product;
|
||||
}
|
||||
else {
|
||||
return device.vendor + QLatin1Char(' ') + device.product;
|
||||
return mtp_device.vendor + QLatin1Char(' ') + mtp_device.product;
|
||||
}
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial];
|
||||
const QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
@@ -708,75 +741,86 @@ QString MacOsDeviceLister::MakeFriendlyName(const QString &serial) {
|
||||
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk));
|
||||
|
||||
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
const QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
const QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
|
||||
if (vendor.isEmpty()) {
|
||||
return product;
|
||||
}
|
||||
|
||||
return vendor + QLatin1Char(' ') + product;
|
||||
|
||||
}
|
||||
|
||||
QList<QUrl> MacOsDeviceLister::MakeDeviceUrls(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
const MTPDevice &device = mtp_devices_[serial];
|
||||
QString str = QString::asprintf("gphoto2://usb-%d-%d/", device.bus, device.address);
|
||||
const MTPDevice &mtp_device = mtp_devices_[serial];
|
||||
const QString str = QString::asprintf("gphoto2://usb-%d-%d/", mtp_device.bus, mtp_device.address);
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem(u"vendor"_s, device.vendor);
|
||||
url_query.addQueryItem(u"vendor_id"_s, QString::number(device.vendor_id));
|
||||
url_query.addQueryItem(u"product"_s, device.product);
|
||||
url_query.addQueryItem(u"product_id"_s, QString::number(device.product_id));
|
||||
url_query.addQueryItem(u"quirks"_s, QString::number(device.quirks));
|
||||
url_query.addQueryItem(u"vendor"_s, mtp_device.vendor);
|
||||
url_query.addQueryItem(u"vendor_id"_s, QString::number(mtp_device.vendor_id));
|
||||
url_query.addQueryItem(u"product"_s, mtp_device.product);
|
||||
url_query.addQueryItem(u"product_id"_s, QString::number(mtp_device.product_id));
|
||||
url_query.addQueryItem(u"quirks"_s, QString::number(mtp_device.quirks));
|
||||
QUrl url(str);
|
||||
url.setQuery(url_query);
|
||||
return QList<QUrl>() << url;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
if (IsCDDevice(serial)) {
|
||||
return QList<QUrl>() << QUrl(u"cdda:///dev/r"_s + serial);
|
||||
}
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk.get())));
|
||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
|
||||
|
||||
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
QUrl ret = MakeUrlFromLocalPath(path);
|
||||
const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
const QUrl ret = MakeUrlFromLocalPath(path);
|
||||
|
||||
return QList<QUrl>() << ret;
|
||||
|
||||
}
|
||||
|
||||
QStringList MacOsDeviceLister::DeviceUniqueIDs() {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
return current_devices_.keys() + mtp_devices_.keys();
|
||||
#else
|
||||
return current_devices_.keys();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
return QVariantList();
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
if (IsCDDevice(serial)) {
|
||||
return QVariantList() << u"media-optical"_s;
|
||||
}
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk.get()));
|
||||
QString icon = GetIconForDevice(device.get());
|
||||
const QString icon = GetIconForDevice(device.get());
|
||||
|
||||
scoped_nsobject<NSDictionary> properties(reinterpret_cast<NSDictionary*>(DADiskCopyDescription(disk)));
|
||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:reinterpret_cast<NSString*>(kDADiskDescriptionVolumePathKey)] copy]);
|
||||
|
||||
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
const QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
|
||||
QVariantList ret;
|
||||
ret << GuessIconForPath(path);
|
||||
@@ -784,31 +828,45 @@ QVariantList MacOsDeviceLister::DeviceIcons(const QString &serial) {
|
||||
if (!icon.isEmpty()) {
|
||||
ret << icon;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QString MacOsDeviceLister::DeviceManufacturer(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
return mtp_devices_[serial].vendor;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString));
|
||||
|
||||
}
|
||||
|
||||
QString MacOsDeviceLister::DeviceModel(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
return mtp_devices_[serial].product;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString));
|
||||
|
||||
}
|
||||
|
||||
quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
return mtp_devices_[serial].capacity;
|
||||
}
|
||||
QString bsd_name = current_devices_[serial];
|
||||
#endif // HAVE_MTP
|
||||
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
@@ -816,7 +874,7 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
||||
|
||||
NSNumber *capacity = reinterpret_cast<NSNumber*>(GetPropertyForDevice(device, CFSTR("Size")));
|
||||
|
||||
quint64 ret = [capacity unsignedLongLongValue];
|
||||
const quint64 ret = [capacity unsignedLongLongValue];
|
||||
|
||||
IOObjectRelease(device);
|
||||
|
||||
@@ -826,10 +884,13 @@ quint64 MacOsDeviceLister::DeviceCapacity(const QString &serial) {
|
||||
|
||||
quint64 MacOsDeviceLister::DeviceFreeSpace(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
return mtp_devices_[serial].free_space;
|
||||
}
|
||||
#endif // HAVE_MTP
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
@@ -857,9 +918,11 @@ bool MacOsDeviceLister::AskForScan(const QString &serial) const {
|
||||
|
||||
void MacOsDeviceLister::UnmountDevice(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) return;
|
||||
#endif
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
const QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, loop_session_, bsd_name.toLatin1().constData()));
|
||||
|
||||
DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this);
|
||||
@@ -879,13 +942,16 @@ void MacOsDeviceLister::DiskUnmountCallback(DADiskRef disk, DADissenterRef disse
|
||||
|
||||
void MacOsDeviceLister::UpdateDeviceFreeSpace(const QString &serial) {
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
if (IsMTPSerial(serial)) {
|
||||
if (mtp_devices_.contains(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice *d = &mtp_devices_[serial];
|
||||
d->free_space = GetFreeSpace(urls[0]);
|
||||
MTPDevice *mtp_device = &mtp_devices_[serial];
|
||||
mtp_device->free_space = GetFreeSpace(urls[0]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Q_EMIT DeviceChanged(serial);
|
||||
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ EditTagDialog::EditTagDialog(const SharedPtr<NetworkAccessManager> network,
|
||||
}
|
||||
else if (RatingBox *ratingbox = qobject_cast<RatingBox*>(widget)) {
|
||||
QObject::connect(ratingbox, &RatingWidget::RatingChanged, this, &EditTagDialog::FieldValueEdited);
|
||||
QObject::connect(ratingbox, &RatingBox::Reset, this, &EditTagDialog::ResetField);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,12 +274,18 @@ EditTagDialog::EditTagDialog(const SharedPtr<NetworkAccessManager> network,
|
||||
QKeySequence(QKeySequence::MoveToNextPage).toString(QKeySequence::NativeText)));
|
||||
|
||||
new TagCompleter(collection_backend, Playlist::Column::Artist, ui_->artist);
|
||||
new TagCompleter(collection_backend, Playlist::Column::ArtistSort, ui_->artistsort);
|
||||
new TagCompleter(collection_backend, Playlist::Column::Album, ui_->album);
|
||||
new TagCompleter(collection_backend, Playlist::Column::AlbumSort, ui_->albumsort);
|
||||
new TagCompleter(collection_backend, Playlist::Column::AlbumArtist, ui_->albumartist);
|
||||
new TagCompleter(collection_backend, Playlist::Column::AlbumArtistSort, ui_->albumartistsort);
|
||||
new TagCompleter(collection_backend, Playlist::Column::Genre, ui_->genre);
|
||||
new TagCompleter(collection_backend, Playlist::Column::Composer, ui_->composer);
|
||||
new TagCompleter(collection_backend, Playlist::Column::ComposerSort, ui_->composersort);
|
||||
new TagCompleter(collection_backend, Playlist::Column::Performer, ui_->performer);
|
||||
new TagCompleter(collection_backend, Playlist::Column::PerformerSort, ui_->performersort);
|
||||
new TagCompleter(collection_backend, Playlist::Column::Grouping, ui_->grouping);
|
||||
new TagCompleter(collection_backend, Playlist::Column::TitleSort, ui_->titlesort);
|
||||
|
||||
}
|
||||
|
||||
@@ -492,11 +499,17 @@ void EditTagDialog::SetSongListVisibility(bool visible) {
|
||||
QVariant EditTagDialog::Data::value(const Song &song, const QString &id) {
|
||||
|
||||
if (id == "title"_L1) return song.title();
|
||||
if (id == "titlesort"_L1) return song.titlesort();
|
||||
if (id == "artist"_L1) return song.artist();
|
||||
if (id == "artistsort"_L1) return song.artistsort();
|
||||
if (id == "album"_L1) return song.album();
|
||||
if (id == "albumsort"_L1) return song.albumsort();
|
||||
if (id == "albumartist"_L1) return song.albumartist();
|
||||
if (id == "albumartistsort"_L1) return song.albumartistsort();
|
||||
if (id == "composer"_L1) return song.composer();
|
||||
if (id == "composersort"_L1) return song.composersort();
|
||||
if (id == "performer"_L1) return song.performer();
|
||||
if (id == "performersort"_L1) return song.performersort();
|
||||
if (id == "grouping"_L1) return song.grouping();
|
||||
if (id == "genre"_L1) return song.genre();
|
||||
if (id == "comment"_L1) return song.comment();
|
||||
@@ -514,11 +527,17 @@ QVariant EditTagDialog::Data::value(const Song &song, const QString &id) {
|
||||
void EditTagDialog::Data::set_value(const QString &id, const QVariant &value) {
|
||||
|
||||
if (id == "title"_L1) current_.set_title(value.toString());
|
||||
else if (id == "titlesort"_L1) current_.set_titlesort(value.toString());
|
||||
else if (id == "artist"_L1) current_.set_artist(value.toString());
|
||||
else if (id == "artistsort"_L1) current_.set_artistsort(value.toString());
|
||||
else if (id == "album"_L1) current_.set_album(value.toString());
|
||||
else if (id == "albumsort"_L1) current_.set_albumsort(value.toString());
|
||||
else if (id == "albumartist"_L1) current_.set_albumartist(value.toString());
|
||||
else if (id == "albumartistsort"_L1) current_.set_albumartistsort(value.toString());
|
||||
else if (id == "composer"_L1) current_.set_composer(value.toString());
|
||||
else if (id == "composersort"_L1) current_.set_composersort(value.toString());
|
||||
else if (id == "performer"_L1) current_.set_performer(value.toString());
|
||||
else if (id == "performersort"_L1) current_.set_performersort(value.toString());
|
||||
else if (id == "grouping"_L1) current_.set_grouping(value.toString());
|
||||
else if (id == "genre"_L1) current_.set_genre(value.toString());
|
||||
else if (id == "comment"_L1) current_.set_comment(value.toString());
|
||||
@@ -544,6 +563,20 @@ bool EditTagDialog::DoesValueVary(const QModelIndexList &sel, const QString &id)
|
||||
|
||||
bool EditTagDialog::IsValueModified(const QModelIndexList &sel, const QString &id) const {
|
||||
|
||||
if (id == u"track"_s || id == u"disc"_s || id == u"year"_s) {
|
||||
return std::any_of(sel.begin(), sel.end(), [this, id](const QModelIndex &i) {
|
||||
const int original = data_[i.row()].original_value(id).toInt();
|
||||
const int current = data_[i.row()].current_value(id).toInt();
|
||||
return original != current && (original != -1 || current != 0);
|
||||
});
|
||||
}
|
||||
else if (id == u"rating"_s) {
|
||||
return std::any_of(sel.begin(), sel.end(), [this, id](const QModelIndex &i) {
|
||||
const float original = data_[i.row()].original_value(id).toFloat();
|
||||
const float current = data_[i.row()].current_value(id).toFloat();
|
||||
return original != current && (original != -1 || current != 0);
|
||||
});
|
||||
}
|
||||
return std::any_of(sel.begin(), sel.end(), [this, id](const QModelIndex &i) { return data_[i.row()].original_value(id) != data_[i.row()].current_value(id); });
|
||||
|
||||
}
|
||||
@@ -605,7 +638,15 @@ void EditTagDialog::UpdateModifiedField(const FieldData &field, const QModelInde
|
||||
QFont new_font(font());
|
||||
new_font.setBold(modified);
|
||||
field.label_->setFont(new_font);
|
||||
if (field.editor_) field.editor_->setFont(new_font);
|
||||
if (field.editor_) {
|
||||
if (ExtendedEditor *editor = dynamic_cast<ExtendedEditor*>(field.editor_)) {
|
||||
editor->set_font(new_font);
|
||||
editor->set_reset_button(modified);
|
||||
}
|
||||
else {
|
||||
field.editor_->setFont(new_font);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -652,14 +693,20 @@ void EditTagDialog::SelectionChanged() {
|
||||
bool art_different = false;
|
||||
bool action_different = false;
|
||||
bool albumartist_enabled = false;
|
||||
bool albumartistsort_enabled = false;
|
||||
bool composer_enabled = false;
|
||||
bool composersort_enabled = false;
|
||||
bool performer_enabled = false;
|
||||
bool performersort_enabled = false;
|
||||
bool grouping_enabled = false;
|
||||
bool genre_enabled = false;
|
||||
bool compilation_enabled = false;
|
||||
bool rating_enabled = false;
|
||||
bool comment_enabled = false;
|
||||
bool lyrics_enabled = false;
|
||||
bool titlesort_enabled = false;
|
||||
bool artistsort_enabled = false;
|
||||
bool albumsort_enabled = false;
|
||||
for (const QModelIndex &idx : indexes) {
|
||||
if (data_.value(idx.row()).cover_action_ == UpdateCoverAction::None) {
|
||||
data_[idx.row()].cover_result_ = AlbumCoverImageResult();
|
||||
@@ -679,12 +726,21 @@ void EditTagDialog::SelectionChanged() {
|
||||
if (song.albumartist_supported()) {
|
||||
albumartist_enabled = true;
|
||||
}
|
||||
if (song.albumartistsort_supported()) {
|
||||
albumartistsort_enabled = true;
|
||||
}
|
||||
if (song.composer_supported()) {
|
||||
composer_enabled = true;
|
||||
}
|
||||
if (song.composersort_supported()) {
|
||||
composersort_enabled = true;
|
||||
}
|
||||
if (song.performer_supported()) {
|
||||
performer_enabled = true;
|
||||
}
|
||||
if (song.performersort_supported()) {
|
||||
performersort_enabled = true;
|
||||
}
|
||||
if (song.grouping_supported()) {
|
||||
grouping_enabled = true;
|
||||
}
|
||||
@@ -703,6 +759,15 @@ void EditTagDialog::SelectionChanged() {
|
||||
if (song.lyrics_supported()) {
|
||||
lyrics_enabled = true;
|
||||
}
|
||||
if (song.titlesort_supported()) {
|
||||
titlesort_enabled = true;
|
||||
}
|
||||
if (song.artistsort_supported()) {
|
||||
artistsort_enabled = true;
|
||||
}
|
||||
if (song.albumsort_supported()) {
|
||||
albumsort_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
QString summary;
|
||||
@@ -759,14 +824,20 @@ void EditTagDialog::SelectionChanged() {
|
||||
album_cover_choice_controller_->set_save_embedded_cover_override(embedded_cover);
|
||||
|
||||
ui_->albumartist->setEnabled(albumartist_enabled);
|
||||
ui_->albumartistsort->setEnabled(albumartistsort_enabled);
|
||||
ui_->composer->setEnabled(composer_enabled);
|
||||
ui_->composersort->setEnabled(composersort_enabled);
|
||||
ui_->performer->setEnabled(performer_enabled);
|
||||
ui_->performersort->setEnabled(performersort_enabled);
|
||||
ui_->grouping->setEnabled(grouping_enabled);
|
||||
ui_->genre->setEnabled(genre_enabled);
|
||||
ui_->compilation->setEnabled(compilation_enabled);
|
||||
ui_->rating->setEnabled(rating_enabled);
|
||||
ui_->comment->setEnabled(comment_enabled);
|
||||
ui_->lyrics->setEnabled(lyrics_enabled);
|
||||
ui_->titlesort->setEnabled(titlesort_enabled);
|
||||
ui_->artistsort->setEnabled(artistsort_enabled);
|
||||
ui_->albumsort->setEnabled(albumsort_enabled);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>801</width>
|
||||
<height>918</height>
|
||||
<width>781</width>
|
||||
<height>1047</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
@@ -700,54 +700,35 @@
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<item row="4" column="3">
|
||||
<widget class="SpinBox" name="year">
|
||||
<property name="correctionMode">
|
||||
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>9999</number>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="14" column="1">
|
||||
<widget class="LineEdit" name="genre">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QLabel" name="label_disc">
|
||||
<widget class="QLabel" name="label_year">
|
||||
<property name="text">
|
||||
<string>Disc</string>
|
||||
<string>Year</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>disc</cstring>
|
||||
<cstring>year</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="LineEdit" name="composer">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
<item row="25" column="1">
|
||||
<widget class="QPushButton" name="fetch_tag">
|
||||
<property name="text">
|
||||
<string>Complete tags automatically</string>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
<property name="icon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/pictures/musicbrainz.png</normaloff>:/pictures/musicbrainz.png</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>38</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="0">
|
||||
<widget class="QLabel" name="label_grouping">
|
||||
<widget class="QLabel" name="label_performersort">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
@@ -755,14 +736,56 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Grouping</string>
|
||||
<string>Performer sort</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>grouping</cstring>
|
||||
<cstring>performersort</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="19" column="0">
|
||||
<widget class="QLabel" name="label_compilation">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Compilation</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>compilation</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="LineEdit" name="albumartistsort">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_titlesort">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Title sort</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>titlesort</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_albumartist">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
@@ -778,7 +801,130 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="1">
|
||||
<item row="0" column="1">
|
||||
<widget class="LineEdit" name="title">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="0">
|
||||
<widget class="QLabel" name="label_performer">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Performer</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>performer</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="18" column="1">
|
||||
<widget class="LineEdit" name="genre">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="SpinBox" name="year">
|
||||
<property name="correctionMode">
|
||||
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>9999</number>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="20" column="1">
|
||||
<widget class="RatingBox" name="rating" native="true">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>140</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="LineEdit" name="album">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="26" column="1">
|
||||
<widget class="TextEdit" name="comment">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="tabChangesFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="LineEdit" name="artistsort">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_title">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>title</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="17" column="1">
|
||||
<widget class="LineEdit" name="grouping">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
@@ -788,7 +934,63 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<item row="1" column="3">
|
||||
<widget class="SpinBox" name="disc">
|
||||
<property name="correctionMode">
|
||||
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>9999</number>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="19" column="1">
|
||||
<widget class="CheckBox" name="compilation">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="LineEdit" name="titlesort">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="20" column="0">
|
||||
<widget class="QLabel" name="label_rating">
|
||||
<property name="text">
|
||||
<string>Rating</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>rating</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLabel" name="label_disc">
|
||||
<property name="text">
|
||||
<string>Disc</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>disc</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<widget class="LineEdit" name="performer">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
@@ -798,26 +1000,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="LineEdit" name="artist">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="LineEdit" name="title">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="SpinBox" name="track">
|
||||
<property name="correctionMode">
|
||||
@@ -834,7 +1016,49 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_albumartistsort">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Album artist sort</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>albumartistsort</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="LineEdit" name="artist">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_artistsort">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Artist sort</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>artistsort</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_album">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
@@ -850,18 +1074,18 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QLabel" name="label_year">
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="label_track">
|
||||
<property name="text">
|
||||
<string>Year</string>
|
||||
<string>Track</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>year</cstring>
|
||||
<cstring>track</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_title">
|
||||
<item row="10" column="0">
|
||||
<widget class="QLabel" name="label_composer">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
@@ -869,10 +1093,72 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Title</string>
|
||||
<string>Composer</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>title</cstring>
|
||||
<cstring>composer</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="LineEdit" name="albumartist">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="1">
|
||||
<widget class="LineEdit" name="composersort">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="1">
|
||||
<widget class="LineEdit" name="performersort">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="17" column="0">
|
||||
<widget class="QLabel" name="label_grouping">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Grouping</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>grouping</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_albumsort">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Album sort</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>albumsort</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -892,66 +1178,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_composer">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Composer</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>composer</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="21" column="1">
|
||||
<widget class="QPushButton" name="fetch_tag">
|
||||
<property name="text">
|
||||
<string>Complete tags automatically</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/pictures/musicbrainz.png</normaloff>:/pictures/musicbrainz.png</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>38</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="SpinBox" name="disc">
|
||||
<property name="correctionMode">
|
||||
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>9999</number>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="15" column="1">
|
||||
<widget class="CheckBox" name="compilation">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="14" column="0">
|
||||
<item row="18" column="0">
|
||||
<widget class="QLabel" name="label_genre">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
@@ -967,7 +1194,17 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="22" column="0">
|
||||
<item row="10" column="1">
|
||||
<widget class="LineEdit" name="composer">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="26" column="0">
|
||||
<widget class="QLabel" name="label_comment">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
@@ -983,44 +1220,8 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="LineEdit" name="album">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="LineEdit" name="albumartist">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="22" column="1">
|
||||
<widget class="TextEdit" name="comment">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="label_performer">
|
||||
<item row="11" column="0">
|
||||
<widget class="QLabel" name="label_composersort">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
@@ -1028,52 +1229,23 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Performer</string>
|
||||
<string>Composer sort</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>performer</cstring>
|
||||
<cstring>composersort</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="15" column="0">
|
||||
<widget class="QLabel" name="label_compilation">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
<item row="6" column="1">
|
||||
<widget class="LineEdit" name="albumsort">
|
||||
<property name="has_reset_button" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Compilation</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>compilation</cstring>
|
||||
<property name="has_clear_button" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="label_track">
|
||||
<property name="text">
|
||||
<string>Track</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>track</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="16" column="0">
|
||||
<widget class="QLabel" name="label_rating">
|
||||
<property name="text">
|
||||
<string>Rating</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>rating</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="16" column="1">
|
||||
<widget class="RatingBox" name="rating" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
@@ -1131,12 +1303,12 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="21" column="1">
|
||||
<widget class="QPushButton" name="fetch_lyrics">
|
||||
<property name="text">
|
||||
<string>Complete lyrics automatically</string>
|
||||
</property>
|
||||
</widget>
|
||||
<item>
|
||||
<widget class="QPushButton" name="fetch_lyrics">
|
||||
<property name="text">
|
||||
<string>Complete lyrics automatically</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
@@ -1199,21 +1371,29 @@
|
||||
<tabstop>summary</tabstop>
|
||||
<tabstop>filename</tabstop>
|
||||
<tabstop>path</tabstop>
|
||||
<tabstop>art_embedded</tabstop>
|
||||
<tabstop>art_manual</tabstop>
|
||||
<tabstop>art_automatic</tabstop>
|
||||
<tabstop>art_unset</tabstop>
|
||||
<tabstop>playcount_reset</tabstop>
|
||||
<tabstop>tags_summary</tabstop>
|
||||
<tabstop>tags_art_button</tabstop>
|
||||
<tabstop>checkbox_embedded_cover</tabstop>
|
||||
<tabstop>title</tabstop>
|
||||
<tabstop>track</tabstop>
|
||||
<tabstop>artist</tabstop>
|
||||
<tabstop>titlesort</tabstop>
|
||||
<tabstop>disc</tabstop>
|
||||
<tabstop>album</tabstop>
|
||||
<tabstop>artist</tabstop>
|
||||
<tabstop>year</tabstop>
|
||||
<tabstop>artistsort</tabstop>
|
||||
<tabstop>album</tabstop>
|
||||
<tabstop>albumsort</tabstop>
|
||||
<tabstop>albumartist</tabstop>
|
||||
<tabstop>albumartistsort</tabstop>
|
||||
<tabstop>composer</tabstop>
|
||||
<tabstop>composersort</tabstop>
|
||||
<tabstop>performer</tabstop>
|
||||
<tabstop>performersort</tabstop>
|
||||
<tabstop>grouping</tabstop>
|
||||
<tabstop>genre</tabstop>
|
||||
<tabstop>compilation</tabstop>
|
||||
|
||||
@@ -36,11 +36,8 @@ namespace {
|
||||
constexpr char kDiscordApplicationId[] = "1352351827206733974";
|
||||
constexpr char kStrawberryIconResourceName[] = "embedded_cover";
|
||||
constexpr char kStrawberryIconDescription[] = "Strawberry Music Player";
|
||||
constexpr qint64 kDiscordPresenceUpdateRateLimitMs = 2000;
|
||||
} // namespace
|
||||
|
||||
using namespace discord_rpc;
|
||||
|
||||
namespace discord {
|
||||
|
||||
RichPresence::RichPresence(const SharedPtr<Player> player,
|
||||
@@ -49,10 +46,8 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
|
||||
: QObject(parent),
|
||||
player_(player),
|
||||
playlist_manager_(playlist_manager),
|
||||
send_presence_timestamp_(0),
|
||||
enabled_(false) {
|
||||
|
||||
Discord_Initialize(kDiscordApplicationId, nullptr, 1, nullptr);
|
||||
initialized_(false),
|
||||
status_display_type_(0) {
|
||||
|
||||
QObject::connect(&*player_->engine(), &EngineBase::StateChanged, this, &RichPresence::EngineStateChanged);
|
||||
QObject::connect(&*playlist_manager_, &PlaylistManager::CurrentSongChanged, this, &RichPresence::CurrentSongChanged);
|
||||
@@ -63,7 +58,11 @@ RichPresence::RichPresence(const SharedPtr<Player> player,
|
||||
}
|
||||
|
||||
RichPresence::~RichPresence() {
|
||||
Discord_Shutdown();
|
||||
|
||||
if (initialized_) {
|
||||
Discord_Shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RichPresence::ReloadSettings() {
|
||||
@@ -71,18 +70,25 @@ void RichPresence::ReloadSettings() {
|
||||
Settings s;
|
||||
s.beginGroup(DiscordRPCSettings::kSettingsGroup);
|
||||
const bool enabled = s.value(DiscordRPCSettings::kEnabled, false).toBool();
|
||||
status_display_type_ = s.value(DiscordRPCSettings::kStatusDisplayType, static_cast<int>(DiscordRPCSettings::StatusDisplayType::App)).toInt();
|
||||
s.endGroup();
|
||||
|
||||
if (enabled_ && !enabled) {
|
||||
Discord_ClearPresence();
|
||||
if (enabled && !initialized_) {
|
||||
Discord_Initialize(kDiscordApplicationId, nullptr, 0);
|
||||
initialized_ = true;
|
||||
}
|
||||
else if (!enabled && initialized_) {
|
||||
Discord_ClearPresence();
|
||||
Discord_Shutdown();
|
||||
initialized_ = false;
|
||||
}
|
||||
|
||||
enabled_ = enabled;
|
||||
|
||||
}
|
||||
|
||||
void RichPresence::EngineStateChanged(const EngineBase::State state) {
|
||||
|
||||
if (!initialized_) return;
|
||||
|
||||
if (state == EngineBase::State::Playing) {
|
||||
SetTimestamp(player_->engine()->position_nanosec() / kNsecPerSec);
|
||||
SendPresenceUpdate();
|
||||
@@ -95,6 +101,8 @@ void RichPresence::EngineStateChanged(const EngineBase::State state) {
|
||||
|
||||
void RichPresence::CurrentSongChanged(const Song &song) {
|
||||
|
||||
if (!initialized_) return;
|
||||
|
||||
SetTimestamp(0LL);
|
||||
activity_.length_secs = song.length_nanosec() / kNsecPerSec;
|
||||
activity_.title = song.title();
|
||||
@@ -107,34 +115,32 @@ void RichPresence::CurrentSongChanged(const Song &song) {
|
||||
|
||||
void RichPresence::SendPresenceUpdate() {
|
||||
|
||||
if (!enabled_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const qint64 current_timestamp = QDateTime::currentMSecsSinceEpoch();
|
||||
if (current_timestamp - send_presence_timestamp_ < kDiscordPresenceUpdateRateLimitMs) {
|
||||
qLog(Info) << "Not sending rich presence due to rate limit of" << kDiscordPresenceUpdateRateLimitMs << "ms";
|
||||
return;
|
||||
}
|
||||
|
||||
send_presence_timestamp_ = current_timestamp;
|
||||
if (!initialized_) return;
|
||||
|
||||
::DiscordRichPresence presence_data{};
|
||||
memset(&presence_data, 0, sizeof(presence_data));
|
||||
presence_data.type = 2; // Listening
|
||||
|
||||
// Listening to
|
||||
presence_data.type = 2;
|
||||
presence_data.status_display_type = status_display_type_;
|
||||
|
||||
presence_data.largeImageKey = kStrawberryIconResourceName;
|
||||
presence_data.smallImageKey = kStrawberryIconResourceName;
|
||||
presence_data.smallImageText = kStrawberryIconDescription;
|
||||
presence_data.instance = 0;
|
||||
|
||||
QByteArray artist;
|
||||
if (!activity_.artist.isEmpty()) {
|
||||
QByteArray artist = activity_.artist.toUtf8();
|
||||
artist.prepend(tr("by ").toUtf8());
|
||||
artist = activity_.artist.toUtf8();
|
||||
if (artist.size() < 2) { // Discord activity 2 char min. fix
|
||||
artist.append(" ");
|
||||
}
|
||||
presence_data.state = artist.constData();
|
||||
}
|
||||
|
||||
if (!activity_.album.isEmpty() && activity_.album != activity_.title) {
|
||||
QByteArray album = activity_.album.toUtf8();
|
||||
QByteArray album;
|
||||
if (!activity_.album.isEmpty()) {
|
||||
album = activity_.album.toUtf8();
|
||||
album.prepend(tr("on ").toUtf8());
|
||||
presence_data.largeImageText = album.constData();
|
||||
}
|
||||
@@ -151,13 +157,19 @@ void RichPresence::SendPresenceUpdate() {
|
||||
}
|
||||
|
||||
void RichPresence::SetTimestamp(const qint64 seconds) {
|
||||
|
||||
activity_.start_timestamp = QDateTime::currentSecsSinceEpoch();
|
||||
activity_.seek_secs = seconds;
|
||||
|
||||
}
|
||||
|
||||
void RichPresence::Seeked(const qint64 seek_microseconds) {
|
||||
|
||||
if (!initialized_) return;
|
||||
|
||||
SetTimestamp(seek_microseconds / 1000LL);
|
||||
SendPresenceUpdate();
|
||||
|
||||
}
|
||||
|
||||
} // namespace discord
|
||||
|
||||
@@ -69,8 +69,8 @@ class RichPresence : public QObject {
|
||||
qint64 seek_secs;
|
||||
};
|
||||
Activity activity_;
|
||||
qint64 send_presence_timestamp_;
|
||||
bool enabled_;
|
||||
bool initialized_;
|
||||
int status_display_type_;
|
||||
};
|
||||
|
||||
} // namespace discord
|
||||
|
||||
@@ -256,3 +256,19 @@ bool EngineBase::ValidOutput(const QString &output) {
|
||||
return (true);
|
||||
|
||||
}
|
||||
|
||||
void EngineBase::UpdateSpotifyAccessToken(const QString &spotify_access_token) {
|
||||
|
||||
#ifdef HAVE_SPOTIFY
|
||||
|
||||
spotify_access_token_ = spotify_access_token;
|
||||
|
||||
SetSpotifyAccessToken();
|
||||
|
||||
#else
|
||||
|
||||
Q_UNUSED(spotify_access_token)
|
||||
|
||||
#endif // HAVE_SPOTIFY
|
||||
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ class EngineBase : public QObject {
|
||||
virtual void ReloadSettings();
|
||||
void UpdateVolume(const uint volume);
|
||||
void EmitAboutToFinish();
|
||||
void UpdateSpotifyAccessToken(const QString &spotify_access_token);
|
||||
|
||||
public:
|
||||
// Simple accessors
|
||||
@@ -175,6 +176,11 @@ class EngineBase : public QObject {
|
||||
|
||||
void Finished();
|
||||
|
||||
private:
|
||||
#ifdef HAVE_SPOTIFY
|
||||
virtual void SetSpotifyAccessToken() {}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool playbin3_enabled_;
|
||||
bool exclusive_mode_;
|
||||
|
||||
@@ -517,10 +517,20 @@ bool GstEngine::ExclusiveModeSupport(const QString &output) const {
|
||||
|
||||
void GstEngine::ReloadSettings() {
|
||||
|
||||
#ifdef HAVE_SPOTIFY
|
||||
const QString old_spotify_access_token = spotify_access_token_;
|
||||
#endif
|
||||
|
||||
EngineBase::ReloadSettings();
|
||||
|
||||
if (output_.isEmpty()) output_ = QLatin1String(kAutoSink);
|
||||
|
||||
#ifdef HAVE_SPOTIFY
|
||||
if (current_pipeline_ && old_spotify_access_token != spotify_access_token_) {
|
||||
current_pipeline_->set_spotify_access_token(spotify_access_token_);
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void GstEngine::ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) {
|
||||
@@ -1199,3 +1209,13 @@ bool GstEngine::AnyExclusivePipelineActive() const {
|
||||
return (current_pipeline_ && current_pipeline_->exclusive_mode()) || OldExclusivePipelineActive();
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_SPOTIFY
|
||||
void GstEngine::SetSpotifyAccessToken() {
|
||||
|
||||
if (current_pipeline_) {
|
||||
current_pipeline_->set_spotify_access_token(spotify_access_token_);
|
||||
}
|
||||
|
||||
}
|
||||
#endif // HAVE_SPOTIFY
|
||||
|
||||
@@ -146,6 +146,10 @@ class GstEngine : public EngineBase, public GstBufferConsumer {
|
||||
bool OldExclusivePipelineActive() const;
|
||||
bool AnyExclusivePipelineActive() const;
|
||||
|
||||
#ifdef HAVE_SPOTIFY
|
||||
void SetSpotifyAccessToken() override;
|
||||
#endif
|
||||
|
||||
private:
|
||||
SharedPtr<TaskManager> task_manager_;
|
||||
GstDiscoverer *discoverer_;
|
||||
|
||||
@@ -1369,6 +1369,12 @@ void GstEnginePipeline::AboutToFinishCallback(GstPlayBin *playbin, gpointer self
|
||||
qLog(Debug) << "Stream from URL" << instance->gst_url_ << "about to finish.";
|
||||
}
|
||||
|
||||
// When playing GME files it seems playbin3 emits about-to-finish early
|
||||
// This stops us from skipping when the song has just started.
|
||||
if (instance->position() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance->about_to_finish_ = true;
|
||||
|
||||
if (instance->HasNextUrl() && !instance->next_uri_set_.value()) {
|
||||
|
||||
@@ -51,7 +51,7 @@ QVariant FilterTree::DataFromColumn(const QString &column, const Song &metadata)
|
||||
if (column == "playcount"_L1) return metadata.playcount();
|
||||
if (column == "skipcount"_L1) return metadata.skipcount();
|
||||
if (column == "filename"_L1) return metadata.basefilename();
|
||||
if (column == "url"_L1) return metadata.effective_stream_url().toString();
|
||||
if (column == "url"_L1) return metadata.effective_url().toString();
|
||||
|
||||
return QVariant();
|
||||
|
||||
|
||||
@@ -66,7 +66,8 @@ void ChartLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id,
|
||||
QObject::disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
const QScopeGuard search_finished = qScopeGuard([this, id]() { Q_EMIT SearchFinished(id); });
|
||||
LyricsSearchResults results;
|
||||
const QScopeGuard search_finished = qScopeGuard([this, id, &results]() { Q_EMIT SearchFinished(id, results); });
|
||||
|
||||
const ReplyDataResult reply_data_result = GetReplyData(reply);
|
||||
if (!reply_data_result.success()) {
|
||||
@@ -75,9 +76,7 @@ void ChartLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id,
|
||||
}
|
||||
|
||||
QXmlStreamReader reader(reply_data_result.data);
|
||||
LyricsSearchResults results;
|
||||
LyricsSearchResult result;
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
const QXmlStreamReader::TokenType type = reader.readNext();
|
||||
const QString name = reader.name().toString();
|
||||
|
||||
@@ -31,14 +31,12 @@
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonParseError>
|
||||
#include <QMessageBox>
|
||||
#include <QMutexLocker>
|
||||
|
||||
#include "includes/shared_ptr.h"
|
||||
#include "core/logging.h"
|
||||
@@ -148,6 +146,8 @@ void GeniusLyricsProvider::StartSearch(const int id, const LyricsSearchRequest &
|
||||
QNetworkReply *reply = CreateGetRequest(QUrl(QLatin1String(kUrlSearch)), url_query);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, id]() { HandleSearchReply(reply, id); });
|
||||
|
||||
qLog(Debug) << name_ << "Sending request for" << url_query.query();
|
||||
|
||||
}
|
||||
|
||||
GeniusLyricsProvider::JsonObjectResult GeniusLyricsProvider::ParseJsonObject(QNetworkReply *reply) {
|
||||
@@ -302,10 +302,8 @@ void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
const QString artist = primary_artist["name"_L1].toString();
|
||||
const QString title = object_result["title"_L1].toString();
|
||||
|
||||
// Ignore results where both the artist and title don't match.
|
||||
if (!artist.startsWith(search->request.albumartist, Qt::CaseInsensitive) &&
|
||||
!artist.startsWith(search->request.artist, Qt::CaseInsensitive) &&
|
||||
!title.startsWith(search->request.title, Qt::CaseInsensitive)) {
|
||||
// Ignore results where the artist or title don't begin or end the same
|
||||
if (!StartsOrEndsMatch(artist, search->request.artist) || !StartsOrEndsMatch(title, search->request.title)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -323,6 +321,12 @@ void GeniusLyricsProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
QNetworkReply *new_reply = CreateGetRequest(url);
|
||||
QObject::connect(new_reply, &QNetworkReply::finished, this, [this, new_reply, search, url]() { HandleLyricReply(new_reply, search->id, url); });
|
||||
|
||||
qLog(Debug) << name_ << "Sending request for" << url;
|
||||
|
||||
// If full match, don't bother iterating further
|
||||
if (artist == search->request.albumartist && artist == search->request.artist && title == search->request.title) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -363,12 +367,18 @@ void GeniusLyricsProvider::HandleLyricReply(QNetworkReply *reply, const int sear
|
||||
return;
|
||||
}
|
||||
|
||||
const QString content = QString::fromUtf8(data);
|
||||
QString lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(content, QRegularExpression(u"<div[^>]*>"_s), QRegularExpression(u"<\\/div>"_s), QRegularExpression(u"<div data-lyrics-container=[^>]+>"_s), true);
|
||||
if (lyrics.isEmpty()) {
|
||||
lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(content, QRegularExpression(u"<div[^>]*>"_s), QRegularExpression(u"<\\/div>"_s), QRegularExpression(u"<div class=\"lyrics\">"_s), true);
|
||||
}
|
||||
static const QRegularExpression start_tag(u"<div[^>]*>"_s);
|
||||
static const QRegularExpression end_tag(u"<\\/div>"_s);
|
||||
static const QRegularExpression lyrics_start(u"<div data-lyrics-container=[^>]+>"_s);
|
||||
|
||||
static const QRegularExpression regex_html_tag_span_trans(u"<span class=\"LyricsHeader__Translations[^>]*>[^<]*</span>"_s);
|
||||
static const QRegularExpression regex_html_tag_div_ellipsis(u"<div class=\"LyricsHeader__TextEllipsis[^>]*>[^<]*</div>"_s);
|
||||
static const QRegularExpression regex_html_tag_span_contribs(u"<span class=\"ContributorsCreditSong__Contributors[^>]*>[^<]*</span>"_s);
|
||||
static const QRegularExpression regex_html_tag_div_bio(u"<div class=\"SongBioPreview__Container[^>]*>.*?</div>"_s);
|
||||
static const QRegularExpression regex_html_tag_h2(u"<h2 [^>]*>[^<]*</h2>"_s);
|
||||
static const QList<QRegularExpression> regex_removes{ regex_html_tag_span_trans, regex_html_tag_div_ellipsis, regex_html_tag_span_contribs, regex_html_tag_div_bio, regex_html_tag_h2 };
|
||||
|
||||
const QString lyrics = HtmlLyricsProvider::ParseLyricsFromHTML(QString::fromUtf8(data), start_tag, end_tag, lyrics_start, true, regex_removes);
|
||||
if (!lyrics.isEmpty()) {
|
||||
LyricsSearchResult result(lyrics);
|
||||
result.artist = lyric.artist;
|
||||
@@ -404,3 +414,17 @@ void GeniusLyricsProvider::EndSearch(const int id, const LyricsSearchRequest &re
|
||||
Q_EMIT SearchFinished(id, results);
|
||||
|
||||
}
|
||||
|
||||
bool GeniusLyricsProvider::StartsOrEndsMatch(QString s, QString t) {
|
||||
|
||||
constexpr Qt::CaseSensitivity cs = Qt::CaseInsensitive;
|
||||
|
||||
static const QRegularExpression puncts_regex(u"[!,.:;]"_s);
|
||||
static const QRegularExpression quotes_regex(u"[’‘´`]"_s);
|
||||
|
||||
s.remove(puncts_regex).replace(quotes_regex, u"'"_s);
|
||||
t.remove(puncts_regex).replace(quotes_regex, u"'"_s);
|
||||
|
||||
return (s.compare(t, cs) == 0 && !s.isEmpty()) || (!s.isEmpty() && !t.isEmpty() && (s.startsWith(t, cs) || t.startsWith(s, cs) || s.endsWith(t, cs) || t.endsWith(s, cs)));
|
||||
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ class GeniusLyricsProvider : public JsonLyricsProvider {
|
||||
void HandleSearchReply(QNetworkReply *reply, const int id);
|
||||
void HandleLyricReply(QNetworkReply *reply, const int search_id, const QUrl &url);
|
||||
|
||||
private:
|
||||
static bool StartsOrEndsMatch(QString s, QString t);
|
||||
|
||||
private:
|
||||
OAuthenticator *oauth_;
|
||||
mutable QMutex mutex_access_token_;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user