Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc07919a75 | ||
|
|
d7661edf67 | ||
|
|
1c91693294 | ||
|
|
3fc3cbc6d5 | ||
|
|
11b5895e69 | ||
|
|
b4c614edbf | ||
|
|
deddaed04a | ||
|
|
a155e503f4 | ||
|
|
c0663bc19f | ||
|
|
7ffa51b83d | ||
|
|
6d397b9988 | ||
|
|
571a7fa26b | ||
|
|
b3b5a38c3a | ||
|
|
a4b115f89b | ||
|
|
3d672bb145 | ||
|
|
15b656b753 | ||
|
|
f5785db163 | ||
|
|
1ff1bf3292 | ||
|
|
b062febea0 | ||
|
|
35301dc79e | ||
|
|
30c336726b | ||
|
|
3821680817 | ||
|
|
74242ea24f | ||
|
|
73c7024e11 | ||
|
|
bbdec92dc6 | ||
|
|
b4c289101c | ||
|
|
722d0797f6 | ||
|
|
deb27d5b55 | ||
|
|
9cc4ffdf6e | ||
|
|
c76d63d1d9 | ||
|
|
21bb4f33ad | ||
|
|
2897b881d6 | ||
|
|
e9b89d0929 | ||
|
|
e801254b2e | ||
|
|
6f49918ee9 | ||
|
|
0ae7c18f1f | ||
|
|
c9c3fb396a | ||
|
|
91db4f1934 | ||
|
|
9bbed6e95c | ||
|
|
748bc27b25 | ||
|
|
f42708e8bc | ||
|
|
160e4570a2 | ||
|
|
6272965143 | ||
|
|
1e9613bf7f | ||
|
|
a061dac298 | ||
|
|
914dee8571 | ||
|
|
47e2905edf | ||
|
|
ee6675aee0 | ||
|
|
62e0d9fe64 | ||
|
|
a174c142c1 | ||
|
|
95afc5fdec | ||
|
|
04d69f66c0 | ||
|
|
0347141edd | ||
|
|
1d6baae6e0 | ||
|
|
fb0f48f08a | ||
|
|
76e5e03d31 | ||
|
|
4cab743634 | ||
|
|
7c10ec97b7 | ||
|
|
8718a16889 | ||
|
|
75a0b924c3 | ||
|
|
83a90e0c05 | ||
|
|
e5eadd1315 | ||
|
|
f0142d90d4 | ||
|
|
cabd6e6e9d | ||
|
|
4804a05736 | ||
|
|
c258e5a3af | ||
|
|
e8492940a5 | ||
|
|
4bccb1ab47 | ||
|
|
b6d219e232 | ||
|
|
23ee17594d | ||
|
|
d9d39d8e25 | ||
|
|
224d5d46c1 | ||
|
|
09e0059930 | ||
|
|
0ddff2b087 | ||
|
|
2e6a29eacc | ||
|
|
ad2fb82aa9 | ||
|
|
a3f91c11e8 | ||
|
|
a50c978ce3 | ||
|
|
27d6f881cd | ||
|
|
944cd020af | ||
|
|
bbe5d64b99 | ||
|
|
f7b36ac4c7 | ||
|
|
1d555ca17e | ||
|
|
f91b6c3468 | ||
|
|
abe6eeb350 | ||
|
|
64f90a7912 | ||
|
|
5733966843 | ||
|
|
eb1344fcec | ||
|
|
c5fb29f00e | ||
|
|
63135b9c54 | ||
|
|
f7c666584e | ||
|
|
6834324de2 | ||
|
|
3a0d59e66f | ||
|
|
ffd2e2188a | ||
|
|
0e8d5bdc5d | ||
|
|
14806f6614 | ||
|
|
00ece83b9d | ||
|
|
8197ae2a2d | ||
|
|
5fe658bb16 | ||
|
|
617179f0c6 | ||
|
|
95ac85f642 | ||
|
|
ca8877ad47 | ||
|
|
6d8f31048c | ||
|
|
ac859eb576 | ||
|
|
2dfa171b6a | ||
|
|
6f72e3e2ea | ||
|
|
c2b73ae963 | ||
|
|
6c50077409 | ||
|
|
06746449a1 | ||
|
|
da7b8edf51 | ||
|
|
6e29b41f23 | ||
|
|
912bb069af | ||
|
|
6b2d7a67d8 | ||
|
|
dbb8ec0290 | ||
|
|
60b32760f2 | ||
|
|
73a40bcb49 | ||
|
|
0e258a5a32 | ||
|
|
7ca65c81d8 | ||
|
|
2ad1a60e59 | ||
|
|
cf17ff4478 | ||
|
|
fffc3aac68 | ||
|
|
295ac3c458 | ||
|
|
b6693a71f9 | ||
|
|
5b21118a8c | ||
|
|
0235b19801 | ||
|
|
7426399aa2 | ||
|
|
6861b0d668 | ||
|
|
c30fb0d38c | ||
|
|
e45521c6c0 | ||
|
|
d11fe8d4fc | ||
|
|
8e83e63e3d | ||
|
|
c8fd0ac4b3 | ||
|
|
d78419eb33 | ||
|
|
e44a3d013d | ||
|
|
aeb0d05017 | ||
|
|
62702e4b3d | ||
|
|
5146cdfa2f | ||
|
|
246e7018c3 | ||
|
|
24286dbe9d | ||
|
|
2f442dfbe1 | ||
|
|
b2fb01ee9c | ||
|
|
45e0a9a4ef | ||
|
|
675b7b4bf4 | ||
|
|
be7a35443e | ||
|
|
9918615fcd | ||
|
|
4b72ef77c1 |
32
.github/workflows/ccpp.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
name: Create source tarball
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: opensuse/leap:15.1
|
||||
image: opensuse/leap:15.2
|
||||
steps:
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: Update packages
|
||||
@@ -352,7 +352,6 @@ jobs:
|
||||
qt6-x11extras-devel
|
||||
qt6-base-common-devel
|
||||
qt6-sql-sqlite
|
||||
qt6-qt5compat-devel
|
||||
libcdio-devel
|
||||
libgpod-devel
|
||||
libmtp-devel
|
||||
@@ -1000,10 +999,10 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- name: Update
|
||||
run: brew update
|
||||
- name: Upgrade
|
||||
run: brew upgrade
|
||||
#- name: Update
|
||||
# run: brew update
|
||||
#- name: Upgrade
|
||||
# run: brew upgrade
|
||||
- name: Install packages
|
||||
run: >
|
||||
brew install
|
||||
@@ -1179,7 +1178,7 @@ jobs:
|
||||
|
||||
- name: Copy extra binaries
|
||||
working-directory: build
|
||||
run: cp /usr/src/strawberry-mxe/usr/x86_64-w64-mingw32.shared/bin/{sqlite3.exe,killproc.exe,liborc-0.4-0.dll} .
|
||||
run: cp /usr/src/strawberry-mxe/usr/x86_64-w64-mingw32.shared/bin/{sqlite3.exe,killproc.exe} .
|
||||
|
||||
- name: Copy dependencies
|
||||
working-directory: build
|
||||
@@ -1230,22 +1229,3 @@ jobs:
|
||||
for i in $(find uploads -type f -name '*.dmg'); do
|
||||
rsync -e "ssh -p 50220 -o StrictHostKeyChecking=no" -va $i travis@echoes.jkvinge.net:/home/travis/builds/macos/catalina/
|
||||
done
|
||||
|
||||
|
||||
build_snap:
|
||||
name: Build Snap
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1.2.0
|
||||
- uses: snapcore/action-build@v1
|
||||
id: snapcraft
|
||||
- uses: snapcore/action-publish@v1
|
||||
if: github.ref == 'refs/heads/master'
|
||||
with:
|
||||
store_login: ${{ secrets.SNAP_STORE_LOGIN }}
|
||||
snap: ${{ steps.snapcraft.outputs.snap }}
|
||||
release: beta
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release_snap
|
||||
path: ${{ steps.snapcraft.outputs.snap }}
|
||||
|
||||
9
3rdparty/singleapplication/CMakeLists.txt
vendored
@@ -16,11 +16,6 @@ else()
|
||||
qt5_wrap_cpp(SINGLEAPP-SOURCES-MOC ${SINGLEAPP-MOC-HEADERS})
|
||||
endif()
|
||||
add_library(singleapplication STATIC ${SINGLEAPP-SOURCES} ${SINGLEAPP-SOURCES-MOC})
|
||||
target_include_directories(singleapplication SYSTEM PRIVATE
|
||||
${QtCore_INCLUDE_DIRS}
|
||||
${QtWidgets_INCLUDE_DIRS}
|
||||
${QtNetwork_INCLUDE_DIRS}
|
||||
)
|
||||
target_include_directories(singleapplication PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
@@ -39,10 +34,6 @@ else()
|
||||
qt5_wrap_cpp(SINGLECOREAPP-SOURCES-MOC ${SINGLECOREAPP-MOC-HEADERS})
|
||||
endif()
|
||||
add_library(singlecoreapplication STATIC ${SINGLECOREAPP-SOURCES} ${SINGLECOREAPP-SOURCES-MOC})
|
||||
target_include_directories(singlecoreapplication SYSTEM PRIVATE
|
||||
${QtCore_INCLUDE_DIRS}
|
||||
${QtNetwork_INCLUDE_DIRS}
|
||||
)
|
||||
target_include_directories(singlecoreapplication PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
|
||||
1
3rdparty/utf8-cpp/CMakeLists.txt
vendored
@@ -1,2 +1 @@
|
||||
cmake_minimum_required(VERSION 3.0)
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
122
CMakeLists.txt
@@ -29,12 +29,12 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
|
||||
set(OPENBSD ON)
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
list(APPEND COMPILE_OPTIONS
|
||||
$<$<COMPILE_LANGUAGE:C>:--std=c99>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:--std=c++11>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:--std=c++17>
|
||||
-U__STRICT_ANSI__
|
||||
-Wall
|
||||
-Wextra
|
||||
@@ -133,6 +133,11 @@ pkg_check_modules(GDK_PIXBUF gdk-pixbuf-2.0)
|
||||
find_package(Gettext)
|
||||
find_package(FFTW3)
|
||||
|
||||
if(NOT QT_DEFAULT_MAJOR_VERSION)
|
||||
set(QT_DEFAULT_MAJOR_VERSION 5)
|
||||
endif()
|
||||
set(QT_MAJOR_VERSION ${QT_DEFAULT_MAJOR_VERSION} CACHE STRING "Qt version to use (5 or 6), defaults to ${QT_DEFAULT_MAJOR_VERSION}")
|
||||
|
||||
option(BUILD_WITH_QT5 "Use Qt 5" OFF)
|
||||
option(BUILD_WITH_QT6 "Use Qt 6" OFF)
|
||||
|
||||
@@ -140,8 +145,19 @@ if(WITH_QT6)
|
||||
set(BUILD_WITH_QT6 ON)
|
||||
endif()
|
||||
|
||||
if(NOT BUILD_WITH_QT5 AND NOT BUILD_WITH_QT6)
|
||||
set(BUILD_WITH_QT5 ON)
|
||||
if(BUILD_WITH_QT5)
|
||||
set(QT_MAJOR_VERSION 5)
|
||||
elseif(BUILD_WITH_QT6)
|
||||
set(QT_MAJOR_VERSION 6)
|
||||
else()
|
||||
if(QT_MAJOR_VERSION EQUAL 5)
|
||||
set(BUILD_WITH_QT5 ON)
|
||||
elseif(QT_MAJOR_VERSION EQUAL 6)
|
||||
set(BUILD_WITH_QT6 ON)
|
||||
else()
|
||||
set(BUILD_WITH_QT5 ON)
|
||||
set(QT_MAJOR_VERSION 5)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(QT_COMPONENTS Core Concurrent Widgets Network Sql)
|
||||
@@ -151,80 +167,35 @@ endif()
|
||||
if(DBUS_FOUND)
|
||||
list(APPEND QT_COMPONENTS DBus)
|
||||
endif()
|
||||
if(APPLE)
|
||||
list(APPEND QT_COMPONENTS MacExtras)
|
||||
endif()
|
||||
if(WIN32)
|
||||
list(APPEND QT_COMPONENTS WinExtras)
|
||||
endif()
|
||||
|
||||
if(BUILD_WITH_QT6)
|
||||
list(APPEND QT_COMPONENTS Core5Compat)
|
||||
find_package(Qt6 REQUIRED COMPONENTS ${QT_COMPONENTS})
|
||||
set(QtCore_LIBRARIES Qt6::Core)
|
||||
set(QtConcurrent_LIBRARIES Qt6::Concurrent)
|
||||
set(QtWidgets_LIBRARIES Qt6::Widgets)
|
||||
set(QtNetwork_LIBRARIES Qt6::Network)
|
||||
set(QtSql_LIBRARIES Qt6::Sql)
|
||||
set(QT_LIBRARIES Qt6::Core Qt6::Concurrent Qt6::Widgets Qt6::Network Qt6::Sql Qt6::Core5Compat)
|
||||
if(Qt6DBus_FOUND)
|
||||
set(QtDBus_LIBRARIES Qt6::DBus)
|
||||
list(APPEND QT_LIBRARIES Qt6::DBus)
|
||||
get_target_property(QT_DBUSXML2CPP_EXECUTABLE Qt6::qdbusxml2cpp LOCATION)
|
||||
endif()
|
||||
if(Qt6X11Extras_FOUND)
|
||||
set(QtX11Extras_LIBRARIES Qt6::X11Extras)
|
||||
list(APPEND QT_LIBRARIES Qt6::X11Extras)
|
||||
endif()
|
||||
if(Qt6MacExtras_FOUND)
|
||||
set(QtMacExtras_LIBRARIES Qt6::MacExtras)
|
||||
list(APPEND QT_LIBRARIES Qt6::MacExtras)
|
||||
endif()
|
||||
if(Qt6WinExtras_FOUND)
|
||||
set(QtWinExtras_LIBRARIES Qt6::WinExtras)
|
||||
list(APPEND QT_LIBRARIES Qt6::WinExtras)
|
||||
endif()
|
||||
find_package(Qt6 QUIET COMPONENTS LinguistTools CONFIG)
|
||||
if (Qt6LinguistTools_FOUND)
|
||||
set(QT_LCONVERT_EXECUTABLE Qt6::lconvert)
|
||||
endif()
|
||||
elseif(BUILD_WITH_QT5)
|
||||
set(QT_MIN_VERSION 5.8)
|
||||
find_package(Qt5 ${QT_MIN_VERSION} REQUIRED COMPONENTS ${QT_COMPONENTS})
|
||||
set(QtCore_LIBRARIES ${Qt5Core_LIBRARIES})
|
||||
set(QtConcurrent_LIBRARIES ${Qt5Concurrent_LIBRARIES})
|
||||
set(QtWidgets_LIBRARIES ${Qt5Widgets_LIBRARIES})
|
||||
set(QtNetwork_LIBRARIES ${Qt5Network_LIBRARIES})
|
||||
set(QtSql_LIBRARIES ${Qt5Sql_LIBRARIES})
|
||||
set(QT_LIBRARIES ${QtCore_LIBRARIES} ${QtConcurrent_LIBRARIES} ${QtWidgets_LIBRARIES} ${QtNetwork_LIBRARIES} ${QtSql_LIBRARIES})
|
||||
set(QT_INCLUDE_DIRS ${Qt5Core_INCLUDE_DIRS} ${Qt5Concurrent_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS} ${Qt5Network_INCLUDE_DIRS} ${Qt5Sql_INCLUDE_DIRS})
|
||||
if(Qt5DBus_FOUND)
|
||||
set(QtDBus_LIBRARIES ${Qt5DBus_LIBRARIES})
|
||||
list(APPEND QT_LIBRARIES ${Qt5DBus_LIBRARIES})
|
||||
list(APPEND QT_INCLUDE_DIRS ${Qt5DBus_INCLUDE_DIRS})
|
||||
get_target_property(QT_DBUSXML2CPP_EXECUTABLE Qt5::qdbusxml2cpp LOCATION)
|
||||
endif()
|
||||
if(Qt5X11Extras_FOUND)
|
||||
set(QtX11Extras_LIBRARIES ${Qt5X11Extras_LIBRARIES})
|
||||
list(APPEND QT_LIBRARIES ${Qt5X11Extras_LIBRARIES})
|
||||
list(APPEND QT_INCLUDE_DIRS ${Qt5X11Extras_INCLUDE_DIRS})
|
||||
endif()
|
||||
if(Qt5MacExtras_FOUND)
|
||||
set(QtMacExtras_LIBRARIES ${Qt5MacExtras_LIBRARIES})
|
||||
list(APPEND QT_LIBRARIES ${Qt5MacExtras_LIBRARIES})
|
||||
list(APPEND QT_INCLUDE_DIRS ${Qt5MacExtras_INCLUDE_DIRS})
|
||||
endif()
|
||||
if(Qt5WinExtras_FOUND)
|
||||
set(QtWinExtras_LIBRARIES ${Qt5WinExtras_LIBRARIES})
|
||||
list(APPEND QT_LIBRARIES ${Qt5WinExtras_LIBRARIES})
|
||||
list(APPEND QT_INCLUDE_DIRS ${Qt5WinExtras_INCLUDE_DIRS})
|
||||
endif()
|
||||
find_package(Qt5 ${QT_MIN_VERSION} QUIET COMPONENTS LinguistTools CONFIG)
|
||||
if (Qt5LinguistTools_FOUND)
|
||||
set(QT_LCONVERT_EXECUTABLE Qt5::lconvert)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "Set BUILD_WITH_QT5 or BUILD_WITH_QT6")
|
||||
find_package(Qt${QT_MAJOR_VERSION} REQUIRED COMPONENTS ${QT_COMPONENTS})
|
||||
|
||||
set(QtCore_LIBRARIES Qt${QT_MAJOR_VERSION}::Core)
|
||||
set(QtConcurrent_LIBRARIES Qt${QT_MAJOR_VERSION}::Concurrent)
|
||||
set(QtWidgets_LIBRARIES Qt${QT_MAJOR_VERSION}::Widgets)
|
||||
set(QtNetwork_LIBRARIES Qt${QT_MAJOR_VERSION}::Network)
|
||||
set(QtSql_LIBRARIES Qt${QT_MAJOR_VERSION}::Sql)
|
||||
set(QT_LIBRARIES Qt${QT_MAJOR_VERSION}::Core Qt${QT_MAJOR_VERSION}::Concurrent Qt${QT_MAJOR_VERSION}::Widgets Qt${QT_MAJOR_VERSION}::Network Qt${QT_MAJOR_VERSION}::Sql)
|
||||
if(Qt${QT_MAJOR_VERSION}DBus_FOUND)
|
||||
set(QtDBus_LIBRARIES Qt${QT_MAJOR_VERSION}::DBus)
|
||||
list(APPEND QT_LIBRARIES Qt${QT_MAJOR_VERSION}::DBus)
|
||||
get_target_property(QT_DBUSXML2CPP_EXECUTABLE Qt${QT_MAJOR_VERSION}::qdbusxml2cpp LOCATION)
|
||||
endif()
|
||||
if(Qt${QT_MAJOR_VERSION}X11Extras_FOUND)
|
||||
set(QtX11Extras_LIBRARIES Qt${QT_MAJOR_VERSION}::X11Extras)
|
||||
list(APPEND QT_LIBRARIES Qt${QT_MAJOR_VERSION}::X11Extras)
|
||||
endif()
|
||||
if(Qt${QT_MAJOR_VERSION}WinExtras_FOUND)
|
||||
set(QtWinExtras_LIBRARIES Qt${QT_MAJOR_VERSION}::WinExtras)
|
||||
list(APPEND QT_LIBRARIES Qt${QT_MAJOR_VERSION}::WinExtras)
|
||||
endif()
|
||||
|
||||
find_package(Qt${QT_MAJOR_VERSION} QUIET COMPONENTS LinguistTools CONFIG)
|
||||
if(Qt${QT_MAJOR_VERSION}LinguistTools_FOUND)
|
||||
set(QT_LCONVERT_EXECUTABLE Qt${QT_MAJOR_VERSION}::lconvert)
|
||||
endif()
|
||||
|
||||
if(X11_FOUND)
|
||||
@@ -404,9 +375,8 @@ endif(USE_BUNDLE AND NOT USE_BUNDLE_DIR)
|
||||
# Check that we have sqlite3 with FTS5
|
||||
|
||||
if(NOT CMAKE_CROSSCOMPILING)
|
||||
set(CMAKE_REQUIRED_FLAGS "--std=c++11")
|
||||
set(CMAKE_REQUIRED_FLAGS "--std=c++17")
|
||||
set(CMAKE_REQUIRED_LIBRARIES ${QtCore_LIBRARIES} ${QtSql_LIBRARIES})
|
||||
set(CMAKE_REQUIRED_INCLUDES ${QtCore_INCLUDE_DIRS} ${QtSql_INCLUDE_DIRS})
|
||||
check_cxx_source_runs("
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
|
||||
41
Changelog
@@ -2,6 +2,47 @@ Strawberry Music Player
|
||||
=======================
|
||||
ChangeLog
|
||||
|
||||
0.8.4:
|
||||
|
||||
Bugfixes:
|
||||
* Fix preventing session logout when window is maxmimized.
|
||||
* Fix empty space in organize window when copying songs/playlists to devices.
|
||||
* Fix crash when opening about dialog in a wayland session.
|
||||
* Fix stretched fancy/side tabbar style issue with adwaita style (Fedora/Gnome).
|
||||
* Fix centering star icon on playlist tabbar.
|
||||
* Fix network proxy settings for streaming.
|
||||
* Fix copy URL to clipboard to handle non-ASCII characters.
|
||||
* Fix HiDPI scaling for glow animation and drag over playlist.
|
||||
* Fix smart playlist search by filename.
|
||||
* Fix single letter collection nodes showing before dividers.
|
||||
|
||||
Enhancements:
|
||||
* Add support for native global shortcuts on KDE.
|
||||
* Add track progress in system tray icon as an option.
|
||||
* Only strip problematic characters in suggested filename when saving a playlist to file.
|
||||
* Change star/unstar playlist to doubleclick instead of singleclick.
|
||||
* Don't edit playlist name on doubleclick in playlists view.
|
||||
* Make context view top label text selectable.
|
||||
* Add setting to change Qt style.
|
||||
* Clear ID3v3 tags that are empty, and clear ID3v1 tags when setting ID3v3 tags.
|
||||
* Remove remaining uses of QTextCodec.
|
||||
* Remove Core5Compat dependency.
|
||||
|
||||
0.8.3:
|
||||
|
||||
Bugfixes:
|
||||
* Fixed updating playing widget song details in small cover mode.
|
||||
* Fixed file extension when transcoding songs.
|
||||
* Fixed updating album cover to collection in edit tag dialog when pressing save.
|
||||
* Fixed songs with empty artist in collection.
|
||||
* Fixed possible crashes with stream discovery.
|
||||
* Fixed setting engine state to null.
|
||||
* Fixed tagreader crash with empty APE tags.
|
||||
* Fixed a gstreamer memory leak.
|
||||
|
||||
Enhancements:
|
||||
* (Windows) Added WASAPI plugin.
|
||||
|
||||
0.8.2:
|
||||
|
||||
Bugfixes:
|
||||
|
||||
@@ -65,11 +65,11 @@ To build Strawberry from source you need the following installed on your system
|
||||
* [GLib](https://developer.gnome.org/glib/)
|
||||
* [Protobuf library and compiler](https://developers.google.com/protocol-buffers/)
|
||||
* [Qt 5.8 or higher (or Qt 6) with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/)
|
||||
* [Qt components X11Extras and DBus for Linux/BSD, MacExtras for macOS and WinExtras for Windows](https://www.qt.io/)
|
||||
* [Qt components X11Extras and D-Bus for Linux/BSD and WinExtras for Windows](https://www.qt.io/)
|
||||
* [SQLite 3.9 or newer with FTS5](https://www.sqlite.org)
|
||||
* [Chromaprint library](https://acoustid.org/chromaprint)
|
||||
* [ALSA library (linux)](https://www.alsa-project.org/)
|
||||
* [DBus (linux)](https://www.freedesktop.org/wiki/Software/dbus/)
|
||||
* [D-Bus (linux)](https://www.freedesktop.org/wiki/Software/dbus/)
|
||||
* [PulseAudio (linux optional)](https://www.freedesktop.org/wiki/Software/PulseAudio/?)
|
||||
* [GStreamer](https://gstreamer.freedesktop.org/) or [VLC](https://www.videolan.org)
|
||||
* [GnuTLS](https://www.gnutls.org/)
|
||||
@@ -84,8 +84,6 @@ Optional dependencies:
|
||||
Either GStreamer or VLC engine is required, but only GStreamer is fully implemented so far.
|
||||
You should also install the gstreamer plugins base and good, and optionally bad and ugly.
|
||||
|
||||
With Qt 6 we also depend on the Core5Compat module for QTextCodec.
|
||||
|
||||
### :wrench: Compiling from source
|
||||
|
||||
### Get the code:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
set(STRAWBERRY_VERSION_MAJOR 0)
|
||||
set(STRAWBERRY_VERSION_MINOR 8)
|
||||
set(STRAWBERRY_VERSION_PATCH 2)
|
||||
set(STRAWBERRY_VERSION_PATCH 4)
|
||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||
|
||||
set(INCLUDE_GIT_REVISION OFF)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<file>style/smartplaylistsearchterm.css</file>
|
||||
<file>html/oauthsuccess.html</file>
|
||||
<file>pictures/strawberry.png</file>
|
||||
<file>pictures/strawberry-grey.png</file>
|
||||
<file>pictures/strawberry-faded.png</file>
|
||||
<file>pictures/strawbs.png</file>
|
||||
<file>pictures/nomusic.png</file>
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
<file>icons/128x128/star-grey.png</file>
|
||||
<file>icons/128x128/star.png</file>
|
||||
<file>icons/128x128/strawberry.png</file>
|
||||
<file>icons/128x128/strawberry-grey.png</file>
|
||||
<file>icons/128x128/tools-wizard.png</file>
|
||||
<file>icons/128x128/view-choose.png</file>
|
||||
<file>icons/128x128/view-fullscreen.png</file>
|
||||
@@ -164,6 +165,7 @@
|
||||
<file>icons/64x64/star-grey.png</file>
|
||||
<file>icons/64x64/star.png</file>
|
||||
<file>icons/64x64/strawberry.png</file>
|
||||
<file>icons/64x64/strawberry-grey.png</file>
|
||||
<file>icons/64x64/tools-wizard.png</file>
|
||||
<file>icons/64x64/view-choose.png</file>
|
||||
<file>icons/64x64/view-fullscreen.png</file>
|
||||
@@ -260,6 +262,7 @@
|
||||
<file>icons/48x48/star-grey.png</file>
|
||||
<file>icons/48x48/star.png</file>
|
||||
<file>icons/48x48/strawberry.png</file>
|
||||
<file>icons/48x48/strawberry-grey.png</file>
|
||||
<file>icons/48x48/tools-wizard.png</file>
|
||||
<file>icons/48x48/view-choose.png</file>
|
||||
<file>icons/48x48/view-fullscreen.png</file>
|
||||
@@ -356,6 +359,7 @@
|
||||
<file>icons/32x32/star-grey.png</file>
|
||||
<file>icons/32x32/star.png</file>
|
||||
<file>icons/32x32/strawberry.png</file>
|
||||
<file>icons/32x32/strawberry-grey.png</file>
|
||||
<file>icons/32x32/tools-wizard.png</file>
|
||||
<file>icons/32x32/view-choose.png</file>
|
||||
<file>icons/32x32/view-fullscreen.png</file>
|
||||
@@ -452,6 +456,7 @@
|
||||
<file>icons/22x22/star-grey.png</file>
|
||||
<file>icons/22x22/star.png</file>
|
||||
<file>icons/22x22/strawberry.png</file>
|
||||
<file>icons/22x22/strawberry-grey.png</file>
|
||||
<file>icons/22x22/tools-wizard.png</file>
|
||||
<file>icons/22x22/view-choose.png</file>
|
||||
<file>icons/22x22/view-fullscreen.png</file>
|
||||
|
||||
BIN
data/icons/128x128/strawberry-grey.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
data/icons/22x22/strawberry-grey.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
data/icons/32x32/strawberry-grey.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
data/icons/48x48/strawberry-grey.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
data/icons/64x64/strawberry-grey.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
data/icons/full/strawberry-grey.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
data/pictures/strawberry-grey.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
1
dist/CMakeLists.txt
vendored
@@ -7,7 +7,6 @@ endif (APPLE)
|
||||
|
||||
if (WIN32)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi.in ${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi @ONLY)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry-wasapi.nsi.in ${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry-wasapi.nsi @ONLY)
|
||||
endif (WIN32)
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
|
||||
3
dist/unix/strawberry.spec.in
vendored
@@ -107,6 +107,9 @@ Features:
|
||||
%setup -qn %{name}-@STRAWBERRY_VERSION_PACKAGE@
|
||||
|
||||
%build
|
||||
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
|
||||
export CXXFLAGS="-fPIC $RPM_OPT_FLAGS"
|
||||
%endif
|
||||
%{cmake} -DCMAKE_BUILD_TYPE:STRING=Release
|
||||
%if 0%{?centos} || 0%{?mageia}
|
||||
%make_build
|
||||
|
||||
182
dist/windows/strawberry-wasapi.nsi.in
vendored
@@ -1,182 +0,0 @@
|
||||
!if "@ARCH@" == x86
|
||||
!define arch_x86
|
||||
!endif
|
||||
|
||||
!if "@ARCH@" == i686-w64-mingw32.shared
|
||||
!define arch_x86
|
||||
!endif
|
||||
|
||||
!if "@ARCH@" == x86_64
|
||||
!define arch_x64
|
||||
!endif
|
||||
|
||||
!if "@ARCH@" == x86_64-w64-mingw32.shared
|
||||
!define arch_x64
|
||||
!endif
|
||||
|
||||
!if "@CMAKE_BUILD_TYPE@" == Debug
|
||||
!define debug
|
||||
!endif
|
||||
|
||||
!if "@BUILD_WITH_QT6@" == "ON"
|
||||
!define with_qt6
|
||||
!endif
|
||||
|
||||
!ifdef debug
|
||||
!define PRODUCT_NAME "Strawberry Music Player Debug WASAPI plugin"
|
||||
!define PRODUCT_NAME_SHORT "StrawberryWASAPI"
|
||||
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME_SHORT}Debug"
|
||||
!ifdef arch_x86
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player Debug"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player Debug"
|
||||
!endif
|
||||
!else
|
||||
!define PRODUCT_NAME "Strawberry Music Player WASAPI plugin"
|
||||
!define PRODUCT_NAME_SHORT "StrawberryWASAPI"
|
||||
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME_SHORT}"
|
||||
!ifdef arch_x86
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES\Strawberry Music Player"
|
||||
!endif
|
||||
!ifdef arch_x64
|
||||
!define PRODUCT_INSTALL_DIR "$PROGRAMFILES64\Strawberry Music Player"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!define PRODUCT_VERSION_MAJOR @STRAWBERRY_VERSION_MAJOR@
|
||||
!define PRODUCT_VERSION_MINOR @STRAWBERRY_VERSION_MINOR@
|
||||
!define PRODUCT_VERSION_PATCH @STRAWBERRY_VERSION_PATCH@
|
||||
!define PRODUCT_DISPLAY_VERSION "@STRAWBERRY_VERSION_PACKAGE@"
|
||||
!define PRODUCT_DISPLAY_VERSION_SHORT "@STRAWBERRY_VERSION_PACKAGE@"
|
||||
|
||||
!define PRODUCT_PUBLISHER "Jonas Kvinge"
|
||||
!define PRODUCT_WEB_SITE "https://www.strawberrymusicplayer.org/"
|
||||
|
||||
!define PRODUCT_UNINST_ROOT_KEY "HKLM"
|
||||
|
||||
SetCompressor /SOLID lzma
|
||||
|
||||
!addplugindir nsisplugins
|
||||
!include "MUI2.nsh"
|
||||
!include "FileAssociation.nsh"
|
||||
!include "Capabilities.nsh"
|
||||
!include LogicLib.nsh
|
||||
!include x64.nsh
|
||||
|
||||
!define MUI_ICON "strawberry.ico"
|
||||
|
||||
!define MUI_COMPONENTSPAGE_SMALLDESC
|
||||
|
||||
; Installer pages
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
; Uninstaller pages
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_UNPAGE_FINISH
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" ;first language is the default language
|
||||
|
||||
Name "${PRODUCT_NAME}"
|
||||
!ifdef arch_x86
|
||||
!ifdef debug
|
||||
!ifdef with_qt6
|
||||
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt6-Debug-x86.exe"
|
||||
!else
|
||||
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Debug-x86.exe"
|
||||
!endif
|
||||
!else
|
||||
!ifdef with_qt6
|
||||
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt6-x86.exe"
|
||||
!else
|
||||
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-x86.exe"
|
||||
!endif
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!ifdef arch_x64
|
||||
!ifdef debug
|
||||
!ifdef with_qt6
|
||||
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt6-Debug-x64.exe"
|
||||
!else
|
||||
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Debug-x64.exe"
|
||||
!endif
|
||||
!else
|
||||
!ifdef with_qt6
|
||||
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt6-x64.exe"
|
||||
!else
|
||||
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-x64.exe"
|
||||
!endif
|
||||
!endif
|
||||
!endif
|
||||
|
||||
InstallDir "${PRODUCT_INSTALL_DIR}"
|
||||
|
||||
; Get the path where Strawberry was installed previously and set it as default path
|
||||
InstallDirRegKey ${PRODUCT_UNINST_ROOT_KEY} ${PRODUCT_UNINST_KEY} "UninstallString"
|
||||
|
||||
ShowInstDetails show
|
||||
ShowUnInstDetails show
|
||||
RequestExecutionLevel admin
|
||||
|
||||
; Check for previous installation, and call the uninstaller if any
|
||||
Function CheckPreviousInstall
|
||||
|
||||
ReadRegStr $R0 ${PRODUCT_UNINST_ROOT_KEY} ${PRODUCT_UNINST_KEY} "UninstallString"
|
||||
StrCmp $R0 "" done
|
||||
|
||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \
|
||||
"${PRODUCT_NAME} is already installed. $\n$\nClick `OK` to remove the \
|
||||
previous version or `Cancel` to cancel this upgrade." \
|
||||
IDOK uninst
|
||||
Abort
|
||||
; Run the uninstaller
|
||||
uninst:
|
||||
ClearErrors
|
||||
ExecWait '$R0' ; Do not copy the uninstaller to a temp file
|
||||
|
||||
done:
|
||||
|
||||
FunctionEnd
|
||||
|
||||
Function .onInit
|
||||
|
||||
!insertmacro MUI_LANGDLL_DISPLAY
|
||||
|
||||
Call CheckPreviousInstall
|
||||
|
||||
FunctionEnd
|
||||
|
||||
Section "Gstreamer wasapi plugin" gstreamer-wasapi-plugin
|
||||
SetOutPath "$INSTDIR\gstreamer-plugins"
|
||||
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstaller"
|
||||
; Create uninstaller
|
||||
WriteUninstaller "$INSTDIR\Uninstall-WASAPI.exe"
|
||||
|
||||
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "${PRODUCT_NAME}"
|
||||
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall-WASAPI.exe"
|
||||
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\strawberry.ico"
|
||||
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_DISPLAY_VERSION}"
|
||||
WriteRegDWORD ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "VersionMajor" "${PRODUCT_VERSION_MAJOR}"
|
||||
WriteRegDWORD ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "VersionMinor" "${PRODUCT_VERSION_MINOR}"
|
||||
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
|
||||
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
|
||||
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
|
||||
Delete "$INSTDIR\gstreamer-plugins\libgstwasapi.dll"
|
||||
Delete "$INSTDIR\Uninstall-WASAPI.exe"
|
||||
|
||||
; Remove the entry from 'installed programs list'
|
||||
DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}"
|
||||
|
||||
SectionEnd
|
||||
26
dist/windows/strawberry.nsi.in
vendored
@@ -238,7 +238,7 @@ Section "Strawberry" Strawberry
|
||||
File "libpcre-1.dll"
|
||||
File "libpcre2-16-0.dll"
|
||||
File "libpng16-16.dll"
|
||||
File "libprotobuf-24.dll"
|
||||
File "libprotobuf-25.dll"
|
||||
File "libpsl-5.dll"
|
||||
File "libsoup-2.4-1.dll"
|
||||
File "libspeex-1.dll"
|
||||
@@ -279,6 +279,16 @@ Section "Strawberry" Strawberry
|
||||
File "libqtsparkle-qt5.dll"
|
||||
!endif
|
||||
|
||||
!ifdef debug
|
||||
File "gdb.exe"
|
||||
File "libdl.dll"
|
||||
File "libexpat-1.dll"
|
||||
File "libmman.dll"
|
||||
File "libmpfr-6.dll"
|
||||
File "libreadline8.dll"
|
||||
File "libtermcap.dll"
|
||||
!endif
|
||||
|
||||
File "killproc.exe"
|
||||
|
||||
; Register Strawberry with Default Programs
|
||||
@@ -361,7 +371,7 @@ Section "Gstreamer plugins" gstreamer-plugins
|
||||
File "/oname=libgsttypefindfunctions.dll" "gstreamer-plugins\libgsttypefindfunctions.dll"
|
||||
File "/oname=libgstgio.dll" "gstreamer-plugins\libgstgio.dll"
|
||||
File "/oname=libgstdirectsound.dll" "gstreamer-plugins\libgstdirectsound.dll"
|
||||
;File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
|
||||
File "/oname=libgstwasapi.dll" "gstreamer-plugins\libgstwasapi.dll"
|
||||
File "/oname=libgstapetag.dll" "gstreamer-plugins\libgstapetag.dll"
|
||||
File "/oname=libgsticydemux.dll" "gstreamer-plugins\libgsticydemux.dll"
|
||||
File "/oname=libgstid3demux.dll" "gstreamer-plugins\libgstid3demux.dll"
|
||||
@@ -496,7 +506,7 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\libpcre-1.dll"
|
||||
Delete "$INSTDIR\libpcre2-16-0.dll"
|
||||
Delete "$INSTDIR\libpng16-16.dll"
|
||||
Delete "$INSTDIR\libprotobuf-24.dll"
|
||||
Delete "$INSTDIR\libprotobuf-25.dll"
|
||||
Delete "$INSTDIR\libpsl-5.dll"
|
||||
Delete "$INSTDIR\libqtsparkle-qt5.dll"
|
||||
Delete "$INSTDIR\libqtsparkle-qt6.dll"
|
||||
@@ -533,6 +543,16 @@ Section "Uninstall"
|
||||
Delete "$INSTDIR\swscale-5.dll"
|
||||
Delete "$INSTDIR\zlib1.dll"
|
||||
|
||||
!ifdef debug
|
||||
Delete "$INSTDIR\gdb.exe"
|
||||
Delete "$INSTDIR\libdl.dll"
|
||||
Delete "$INSTDIR\libexpat-1.dll"
|
||||
Delete "$INSTDIR\libmman.dll"
|
||||
Delete "$INSTDIR\libmpfr-6.dll"
|
||||
Delete "$INSTDIR\libreadline8.dll"
|
||||
Delete "$INSTDIR\libtermcap.dll"
|
||||
!endif
|
||||
|
||||
Delete "$INSTDIR\gio-modules\libgiognutls.dll"
|
||||
Delete "$INSTDIR\platforms\qwindows.dll"
|
||||
Delete "$INSTDIR\sqldrivers\qsqlite.dll"
|
||||
|
||||
@@ -9,7 +9,6 @@ link_directories(
|
||||
${GSTREAMER_BASE_LIBRARY_DIRS}
|
||||
${GSTREAMER_AUDIO_LIBRARY_DIRS}
|
||||
${FFTW3_LIBRARY_DIRS}
|
||||
${QtCore_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
add_library(gstmoodbar STATIC ${SOURCES})
|
||||
@@ -21,7 +20,6 @@ target_include_directories(gstmoodbar SYSTEM PRIVATE
|
||||
${GSTREAMER_BASE_INCLUDE_DIRS}
|
||||
${GSTREAMER_AUDIO_INCLUDE_DIRS}
|
||||
${FFTW3_INCLUDE_DIR}
|
||||
${QtCore_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_include_directories(gstmoodbar PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
@@ -115,7 +115,7 @@ static void gst_fastspectrum_init (GstFastSpectrum * spectrum) {
|
||||
spectrum->interval = DEFAULT_INTERVAL;
|
||||
spectrum->bands = DEFAULT_BANDS;
|
||||
|
||||
spectrum->channel_data_initialised = false;
|
||||
spectrum->channel_data_initialized = false;
|
||||
|
||||
g_mutex_init (&spectrum->lock);
|
||||
|
||||
@@ -137,14 +137,14 @@ static void gst_fastspectrum_alloc_channel_data (GstFastSpectrum * spectrum) {
|
||||
QMutexLocker l(klass->fftw_lock);
|
||||
spectrum->plan = fftw_plan_dft_r2c_1d(nfft, spectrum->fft_input, spectrum->fft_output, FFTW_ESTIMATE);
|
||||
}
|
||||
spectrum->channel_data_initialised = true;
|
||||
spectrum->channel_data_initialized = true;
|
||||
|
||||
}
|
||||
|
||||
static void gst_fastspectrum_free_channel_data (GstFastSpectrum * spectrum) {
|
||||
|
||||
GstFastSpectrumClass* klass = reinterpret_cast<GstFastSpectrumClass*>(G_OBJECT_GET_CLASS(spectrum));
|
||||
if (spectrum->channel_data_initialised) {
|
||||
if (spectrum->channel_data_initialized) {
|
||||
{
|
||||
QMutexLocker l(klass->fftw_lock);
|
||||
fftw_destroy_plan(spectrum->plan);
|
||||
@@ -154,7 +154,7 @@ static void gst_fastspectrum_free_channel_data (GstFastSpectrum * spectrum) {
|
||||
delete[] spectrum->input_ring_buffer;
|
||||
delete[] spectrum->spect_magnitude;
|
||||
|
||||
spectrum->channel_data_initialised = false;
|
||||
spectrum->channel_data_initialized = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -422,7 +422,7 @@ static GstFlowReturn gst_fastspectrum_transform_ip (GstBaseTransform *trans, Gst
|
||||
/* If we don't have a FFT context yet (or it was reset due to parameter
|
||||
* changes) get one and allocate memory for everything
|
||||
*/
|
||||
if (!spectrum->channel_data_initialised) {
|
||||
if (!spectrum->channel_data_initialized) {
|
||||
GST_DEBUG_OBJECT (spectrum, "allocating for bands %u", bands);
|
||||
|
||||
gst_fastspectrum_alloc_channel_data (spectrum);
|
||||
|
||||
@@ -65,7 +65,7 @@ struct GstFastSpectrum {
|
||||
GstClockTime message_ts; /* starttime for next message */
|
||||
|
||||
/* <private> */
|
||||
bool channel_data_initialised;
|
||||
bool channel_data_initialized;
|
||||
double* input_ring_buffer;
|
||||
double* fft_input;
|
||||
fftw_complex* fft_output;
|
||||
|
||||
@@ -28,16 +28,12 @@ endif()
|
||||
|
||||
link_directories(
|
||||
${GLIB_LIBRARY_DIRS}
|
||||
${QtCore_LIBRARY_DIRS}
|
||||
${QtNetwork_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
add_library(libstrawberry-common STATIC ${SOURCES} ${MOC})
|
||||
|
||||
target_include_directories(libstrawberry-common SYSTEM PRIVATE
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
${QtCore_INCLUDE_DIRS}
|
||||
${QtNetwork_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_include_directories(libstrawberry-common PRIVATE
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/* This file is part of Strawberry.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Strawberry is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Strawberry is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef CONCURRENTRUN_H
|
||||
#define CONCURRENTRUN_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QFuture>
|
||||
#include <QRunnable>
|
||||
#include <QThreadPool>
|
||||
|
||||
/*
|
||||
The aim of ThreadFunctor classes and ConcurrentRun::Run() functions is to
|
||||
complete QtConcurrentRun, which lack support for using a particular
|
||||
QThreadPool, as it always uses QThreadPool::globalInstance().
|
||||
|
||||
This is problematic when we do not want to share the same thread pool over
|
||||
all the application, but want to keep the convenient QtConcurrent::run()
|
||||
functor syntax.
|
||||
With ConcurrentRun::Run(), time critical changes can be performed in their
|
||||
own pool, which is not empty by other actions (as it happens when using
|
||||
QtConcurrentRun::run()).
|
||||
|
||||
ThreadFunctor classes are used to store a functor and its arguments, and
|
||||
Run() functions are used for convenience: to directly create a new
|
||||
ThreadFunctor object and start it.
|
||||
*/
|
||||
|
||||
/*
|
||||
Base abstract classes ThreadFunctorBase and ThreadFunctor (for void and
|
||||
non-void result):
|
||||
*/
|
||||
template<typename ReturnType>
|
||||
class ThreadFunctorBase : public QFutureInterface<ReturnType>, public QRunnable {
|
||||
public:
|
||||
ThreadFunctorBase() {}
|
||||
|
||||
QFuture<ReturnType> Start(QThreadPool* thread_pool) {
|
||||
this->setRunnable(this);
|
||||
this->reportStarted();
|
||||
Q_ASSERT(thread_pool);
|
||||
QFuture<ReturnType> future = this->future();
|
||||
thread_pool->start(this, 0 /* priority: currently we do not support changing the priority. Might be added later if needed */);
|
||||
return future;
|
||||
}
|
||||
|
||||
void run() override = 0;
|
||||
};
|
||||
|
||||
template <typename ReturnType, typename... Args>
|
||||
class ThreadFunctor : public ThreadFunctorBase<ReturnType> {
|
||||
public:
|
||||
explicit ThreadFunctor(std::function<ReturnType (Args...)> function, Args... args)
|
||||
: function_(std::bind(function, args...)) {
|
||||
}
|
||||
|
||||
void run() override {
|
||||
this->reportResult(function_());
|
||||
this->reportFinished();
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<ReturnType()> function_;
|
||||
};
|
||||
|
||||
// Partial specialisation for void return type.
|
||||
template <typename... Args>
|
||||
class ThreadFunctor <void, Args...> : public ThreadFunctorBase<void> {
|
||||
public:
|
||||
explicit ThreadFunctor(std::function<void (Args...)> function, Args... args)
|
||||
: function_(std::bind(function, args...)) {
|
||||
}
|
||||
|
||||
void run() override {
|
||||
function_();
|
||||
this->reportFinished();
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void()> function_;
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Run functions
|
||||
*/
|
||||
namespace ConcurrentRun {
|
||||
|
||||
// Empty argument form.
|
||||
template <typename ReturnType>
|
||||
QFuture<ReturnType> Run(QThreadPool* threadpool, std::function<ReturnType ()> function) {
|
||||
return (new ThreadFunctor<ReturnType>(function))->Start(threadpool);
|
||||
}
|
||||
|
||||
// Function object with arguments form.
|
||||
template <typename ReturnType, typename... Args>
|
||||
QFuture<ReturnType> Run(QThreadPool* threadpool, std::function<ReturnType (Args...)> function, const Args&... args) {
|
||||
return (new ThreadFunctor<ReturnType, Args...>(function, args...))->Start(threadpool);
|
||||
}
|
||||
|
||||
// Support passing C function pointers instead of function objects.
|
||||
template <typename ReturnType, typename... Args>
|
||||
QFuture<ReturnType> Run(QThreadPool* threadpool, ReturnType (*function) (Args...), const Args&... args) {
|
||||
return Run(threadpool, std::function<ReturnType (Args...)>(function), args...);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // CONCURRENTRUN_H
|
||||
@@ -34,25 +34,25 @@ class Lazy {
|
||||
Lazy() : init_([]() { return new T; }) {}
|
||||
|
||||
T* get() const {
|
||||
CheckInitialised();
|
||||
CheckInitialized();
|
||||
return ptr_.get();
|
||||
}
|
||||
|
||||
typename std::add_lvalue_reference<T>::type operator*() const {
|
||||
CheckInitialised();
|
||||
CheckInitialized();
|
||||
return *ptr_;
|
||||
}
|
||||
|
||||
T* operator->() const { return get(); }
|
||||
|
||||
// Returns true if the object is not yet initialised.
|
||||
// Returns true if the object is not yet initialized.
|
||||
explicit operator bool() const { return ptr_; }
|
||||
|
||||
// Deletes the underlying object and will re-run the initialisation function if the object is requested again.
|
||||
// Deletes the underlying object and will re-run the initialization function if the object is requested again.
|
||||
void reset() { ptr_.reset(); }
|
||||
|
||||
private:
|
||||
void CheckInitialised() const {
|
||||
void CheckInitialized() const {
|
||||
if (!ptr_) {
|
||||
ptr_.reset(init_(), [](T*obj) { obj->deleteLater(); });
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ link_directories(
|
||||
${GLIB_LIBRARY_DIRS}
|
||||
${PROTOBUF_LIBRARY_DIRS}
|
||||
${TAGLIB_LIBRARY_DIRS}
|
||||
${Qt5Core_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
add_library(libstrawberry-tagreader STATIC ${PROTO_SOURCES} ${SOURCES})
|
||||
@@ -17,8 +16,6 @@ add_library(libstrawberry-tagreader STATIC ${PROTO_SOURCES} ${SOURCES})
|
||||
target_include_directories(libstrawberry-tagreader SYSTEM PRIVATE
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
${PROTOBUF_INCLUDE_DIRS}
|
||||
${QtCore_INCLUDE_DIRS}
|
||||
${QtNetwork_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_include_directories(libstrawberry-tagreader PRIVATE
|
||||
@@ -38,7 +35,3 @@ target_link_libraries(libstrawberry-tagreader PRIVATE
|
||||
${QtNetwork_LIBRARIES}
|
||||
libstrawberry-common
|
||||
)
|
||||
|
||||
if(BUILD_WITH_QT6)
|
||||
target_link_libraries(libstrawberry-tagreader PRIVATE Qt6::Core5Compat)
|
||||
endif()
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QTextCodec>
|
||||
#include <QVector>
|
||||
#include <QtDebug>
|
||||
|
||||
@@ -138,6 +137,15 @@ TagReader::~TagReader() {
|
||||
delete factory_;
|
||||
}
|
||||
|
||||
bool TagReader::IsMediaFile(const QString &filename) const {
|
||||
|
||||
qLog(Debug) << "Checking for valid file" << filename;
|
||||
|
||||
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
||||
return !fileref->isNull() && fileref->tag();
|
||||
|
||||
}
|
||||
|
||||
pb::tagreader::SongMetadata_FileType TagReader::GuessFileType(TagLib::FileRef *fileref) const {
|
||||
|
||||
if (dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) return pb::tagreader::SongMetadata_FileType_WAV;
|
||||
@@ -198,10 +206,10 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
|
||||
|
||||
TagLib::Tag *tag = fileref->tag();
|
||||
if (tag) {
|
||||
Decode(tag->title(), nullptr, song->mutable_title());
|
||||
Decode(tag->artist(), nullptr, song->mutable_artist()); // TPE1
|
||||
Decode(tag->album(), nullptr, song->mutable_album());
|
||||
Decode(tag->genre(), nullptr, song->mutable_genre());
|
||||
Decode(tag->title(), song->mutable_title());
|
||||
Decode(tag->artist(), song->mutable_artist()); // TPE1
|
||||
Decode(tag->album(), song->mutable_album());
|
||||
Decode(tag->genre(), song->mutable_genre());
|
||||
song->set_year(tag->year());
|
||||
song->set_track(tag->track());
|
||||
song->set_valid(true);
|
||||
@@ -214,7 +222,7 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
|
||||
// Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same way;
|
||||
// apart, so we keep specific behavior for some formats by adding another "else if" block below.
|
||||
if (TagLib::Ogg::XiphComment *tag_ogg = dynamic_cast<TagLib::Ogg::XiphComment*>(fileref->file()->tag())) {
|
||||
ParseOggTag(tag_ogg->fieldListMap(), nullptr, &disc, &compilation, song);
|
||||
ParseOggTag(tag_ogg->fieldListMap(), &disc, &compilation, song);
|
||||
if (!tag_ogg->pictureList().isEmpty()) {
|
||||
song->set_art_automatic(kEmbeddedCover);
|
||||
}
|
||||
@@ -225,28 +233,28 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
|
||||
song->set_bitdepth(file_flac->audioProperties()->bitsPerSample());
|
||||
|
||||
if (file_flac->xiphComment()) {
|
||||
ParseOggTag(file_flac->xiphComment()->fieldListMap(), nullptr, &disc, &compilation, song);
|
||||
ParseOggTag(file_flac->xiphComment()->fieldListMap(), &disc, &compilation, song);
|
||||
if (!file_flac->pictureList().isEmpty()) {
|
||||
song->set_art_automatic(kEmbeddedCover);
|
||||
}
|
||||
}
|
||||
if (tag) Decode(tag->comment(), nullptr, song->mutable_comment());
|
||||
if (tag) Decode(tag->comment(), song->mutable_comment());
|
||||
}
|
||||
|
||||
else if (TagLib::WavPack::File *file_wavpack = dynamic_cast<TagLib::WavPack::File *>(fileref->file())) {
|
||||
song->set_bitdepth(file_wavpack->audioProperties()->bitsPerSample());
|
||||
if (file_wavpack->tag()) {
|
||||
ParseAPETag(file_wavpack->APETag()->itemListMap(), nullptr, &disc, &compilation, song);
|
||||
if (file_wavpack->APETag()) {
|
||||
ParseAPETag(file_wavpack->APETag()->itemListMap(), &disc, &compilation, song);
|
||||
}
|
||||
if (tag) Decode(tag->comment(), nullptr, song->mutable_comment());
|
||||
if (tag) Decode(tag->comment(), song->mutable_comment());
|
||||
}
|
||||
|
||||
else if (TagLib::APE::File *file_ape = dynamic_cast<TagLib::APE::File*>(fileref->file())) {
|
||||
if (file_ape->tag()) {
|
||||
ParseAPETag(file_ape->APETag()->itemListMap(), nullptr, &disc, &compilation, song);
|
||||
if (file_ape->APETag()) {
|
||||
ParseAPETag(file_ape->APETag()->itemListMap(), &disc, &compilation, song);
|
||||
}
|
||||
song->set_bitdepth(file_ape->audioProperties()->bitsPerSample());
|
||||
if (tag) Decode(tag->comment(), nullptr, song->mutable_comment());
|
||||
if (tag) Decode(tag->comment(), song->mutable_comment());
|
||||
}
|
||||
|
||||
else if (TagLib::MPEG::File *file_mpeg = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
|
||||
@@ -255,22 +263,22 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
|
||||
const TagLib::ID3v2::FrameListMap &map = file_mpeg->ID3v2Tag()->frameListMap();
|
||||
|
||||
if (!map["TPOS"].isEmpty()) disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
|
||||
if (!map["TCOM"].isEmpty()) Decode(map["TCOM"].front()->toString(), nullptr, song->mutable_composer());
|
||||
if (!map["TCOM"].isEmpty()) Decode(map["TCOM"].front()->toString(), song->mutable_composer());
|
||||
|
||||
// content group
|
||||
if (!map["TIT1"].isEmpty()) Decode(map["TIT1"].front()->toString(), nullptr, song->mutable_grouping());
|
||||
if (!map["TIT1"].isEmpty()) Decode(map["TIT1"].front()->toString(), song->mutable_grouping());
|
||||
|
||||
// ID3v2: lead performer/soloist
|
||||
if (!map["TPE1"].isEmpty()) Decode(map["TPE1"].front()->toString(), nullptr, song->mutable_performer());
|
||||
if (!map["TPE1"].isEmpty()) Decode(map["TPE1"].front()->toString(), song->mutable_performer());
|
||||
|
||||
// original artist/performer
|
||||
if (!map["TOPE"].isEmpty()) Decode(map["TOPE"].front()->toString(), nullptr, song->mutable_performer());
|
||||
if (!map["TOPE"].isEmpty()) Decode(map["TOPE"].front()->toString(), song->mutable_performer());
|
||||
|
||||
// Skip TPE1 (which is the artist) here because we already fetched it
|
||||
|
||||
|
||||
// non-standard: Apple, Microsoft
|
||||
if (!map["TPE2"].isEmpty()) Decode(map["TPE2"].front()->toString(), nullptr, song->mutable_albumartist());
|
||||
if (!map["TPE2"].isEmpty()) Decode(map["TPE2"].front()->toString(), song->mutable_albumartist());
|
||||
|
||||
if (!map["TCMP"].isEmpty()) compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed();
|
||||
|
||||
@@ -280,20 +288,20 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
|
||||
}
|
||||
|
||||
if (!map["USLT"].isEmpty()) {
|
||||
Decode(map["USLT"].front()->toString(), nullptr, song->mutable_lyrics());
|
||||
Decode(map["USLT"].front()->toString(), song->mutable_lyrics());
|
||||
}
|
||||
else if (!map["SYLT"].isEmpty()) {
|
||||
Decode(map["SYLT"].front()->toString(), nullptr, song->mutable_lyrics());
|
||||
Decode(map["SYLT"].front()->toString(), song->mutable_lyrics());
|
||||
}
|
||||
|
||||
if (!map["APIC"].isEmpty()) song->set_art_automatic(kEmbeddedCover);
|
||||
|
||||
// Find a suitable comment tag. For now we ignore iTunNORM comments.
|
||||
for (uint i = 0; i < map["COMM"].size(); ++i) {
|
||||
for (uint i = 0 ; i < map["COMM"].size() ; ++i) {
|
||||
const TagLib::ID3v2::CommentsFrame *frame = dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
|
||||
|
||||
if (frame && TStringToQString(frame->description()) != "iTunNORM") {
|
||||
Decode(frame->text(), nullptr, song->mutable_comment());
|
||||
Decode(frame->text(), song->mutable_comment());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -312,7 +320,7 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
|
||||
if (mp4_tag->item("aART").isValid()) {
|
||||
TagLib::StringList album_artists = mp4_tag->item("aART").toStringList();
|
||||
if (!album_artists.isEmpty()) {
|
||||
Decode(album_artists.front(), nullptr, song->mutable_albumartist());
|
||||
Decode(album_artists.front(), song->mutable_albumartist());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,20 +334,20 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
|
||||
}
|
||||
|
||||
if (mp4_tag->item("\251wrt").isValid()) {
|
||||
Decode(mp4_tag->item("\251wrt").toStringList().toString(", "), nullptr, song->mutable_composer());
|
||||
Decode(mp4_tag->item("\251wrt").toStringList().toString(", "), song->mutable_composer());
|
||||
}
|
||||
if (mp4_tag->item("\251grp").isValid()) {
|
||||
Decode(mp4_tag->item("\251grp").toStringList().toString(" "), nullptr, song->mutable_grouping());
|
||||
Decode(mp4_tag->item("\251grp").toStringList().toString(" "), song->mutable_grouping());
|
||||
}
|
||||
if (mp4_tag->item("\251lyr").isValid()) {
|
||||
Decode(mp4_tag->item("\251lyr").toStringList().toString(" "), nullptr, song->mutable_lyrics());
|
||||
Decode(mp4_tag->item("\251lyr").toStringList().toString(" "), song->mutable_lyrics());
|
||||
}
|
||||
|
||||
if (mp4_tag->item(kMP4_OriginalYear_ID).isValid()) {
|
||||
song->set_originalyear(TStringToQString(mp4_tag->item(kMP4_OriginalYear_ID).toStringList().toString('\n')).left(4).toInt());
|
||||
}
|
||||
|
||||
Decode(mp4_tag->comment(), nullptr, song->mutable_comment());
|
||||
Decode(mp4_tag->comment(), song->mutable_comment());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +356,7 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
|
||||
song->set_bitdepth(file_asf->audioProperties()->bitsPerSample());
|
||||
|
||||
if (file_asf->tag()) {
|
||||
Decode(file_asf->tag()->comment(), nullptr, song->mutable_comment());
|
||||
Decode(file_asf->tag()->comment(), song->mutable_comment());
|
||||
}
|
||||
|
||||
const TagLib::ASF::AttributeListMap &attributes_map = file_asf->tag()->attributeListMap();
|
||||
@@ -367,15 +375,15 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
|
||||
}
|
||||
}
|
||||
|
||||
else if (TagLib::MPC::File* file_mpc = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
|
||||
if (file_mpc->tag()) {
|
||||
ParseAPETag(file_mpc->APETag()->itemListMap(), nullptr, &disc, &compilation, song);
|
||||
else if (TagLib::MPC::File *file_mpc = dynamic_cast<TagLib::MPC::File*>(fileref->file())) {
|
||||
if (file_mpc->APETag()) {
|
||||
ParseAPETag(file_mpc->APETag()->itemListMap(), &disc, &compilation, song);
|
||||
}
|
||||
if (tag) Decode(tag->comment(), nullptr, song->mutable_comment());
|
||||
if (tag) Decode(tag->comment(), song->mutable_comment());
|
||||
}
|
||||
|
||||
else if (tag) {
|
||||
Decode(tag->comment(), nullptr, song->mutable_comment());
|
||||
Decode(tag->comment(), song->mutable_comment());
|
||||
}
|
||||
|
||||
if (!disc.isEmpty()) {
|
||||
@@ -414,42 +422,27 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
|
||||
|
||||
}
|
||||
|
||||
void TagReader::Decode(const TagLib::String &tag, const QTextCodec *codec, std::string *output) {
|
||||
|
||||
QString tmp;
|
||||
|
||||
if (codec && tag.isLatin1()) { // Never override UTF-8.
|
||||
const std::string fixed = QString::fromUtf8(tag.toCString(true)).toStdString();
|
||||
tmp = codec->toUnicode(fixed.c_str()).trimmed();
|
||||
}
|
||||
else {
|
||||
tmp = TStringToQString(tag).trimmed();
|
||||
}
|
||||
void TagReader::Decode(const TagLib::String &tag, std::string *output) {
|
||||
|
||||
QString tmp = TStringToQString(tag).trimmed();
|
||||
output->assign(DataCommaSizeFromQString(tmp));
|
||||
|
||||
}
|
||||
|
||||
void TagReader::Decode(const QString &tag, const QTextCodec *codec, std::string *output) {
|
||||
void TagReader::Decode(const QString &tag, std::string *output) {
|
||||
|
||||
if (!codec) {
|
||||
output->assign(DataCommaSizeFromQString(tag));
|
||||
}
|
||||
else {
|
||||
const QString decoded(codec->toUnicode(tag.toUtf8()));
|
||||
output->assign(DataCommaSizeFromQString(decoded));
|
||||
}
|
||||
output->assign(DataCommaSizeFromQString(tag));
|
||||
|
||||
}
|
||||
|
||||
void TagReader::ParseOggTag(const TagLib::Ogg::FieldListMap &map, const QTextCodec *codec, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const {
|
||||
void TagReader::ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const {
|
||||
|
||||
if (!map["COMPOSER"].isEmpty()) Decode(map["COMPOSER"].front(), codec, song->mutable_composer());
|
||||
if (!map["PERFORMER"].isEmpty()) Decode(map["PERFORMER"].front(), codec, song->mutable_performer());
|
||||
if (!map["CONTENT GROUP"].isEmpty()) Decode(map["CONTENT GROUP"].front(), codec, song->mutable_grouping());
|
||||
if (!map["COMPOSER"].isEmpty()) Decode(map["COMPOSER"].front(), song->mutable_composer());
|
||||
if (!map["PERFORMER"].isEmpty()) Decode(map["PERFORMER"].front(), song->mutable_performer());
|
||||
if (!map["CONTENT GROUP"].isEmpty()) Decode(map["CONTENT GROUP"].front(), song->mutable_grouping());
|
||||
|
||||
if (!map["ALBUMARTIST"].isEmpty()) Decode(map["ALBUMARTIST"].front(), codec, song->mutable_albumartist());
|
||||
else if (!map["ALBUM ARTIST"].isEmpty()) Decode(map["ALBUM ARTIST"].front(), codec, song->mutable_albumartist());
|
||||
if (!map["ALBUMARTIST"].isEmpty()) Decode(map["ALBUMARTIST"].front(), song->mutable_albumartist());
|
||||
else if (!map["ALBUM ARTIST"].isEmpty()) Decode(map["ALBUM ARTIST"].front(), song->mutable_albumartist());
|
||||
|
||||
if (!map["ORIGINALDATE"].isEmpty()) song->set_originalyear(TStringToQString(map["ORIGINALDATE"].front()).left(4).toInt());
|
||||
else if (!map["ORIGINALYEAR"].isEmpty()) song->set_originalyear(TStringToQString(map["ORIGINALYEAR"].front()).toInt());
|
||||
@@ -461,20 +454,18 @@ void TagReader::ParseOggTag(const TagLib::Ogg::FieldListMap &map, const QTextCod
|
||||
|
||||
if (!map["FMPS_PLAYCOUNT"].isEmpty() && song->playcount() <= 0) song->set_playcount(TStringToQString( map["FMPS_PLAYCOUNT"].front() ).trimmed().toFloat());
|
||||
|
||||
if (!map["LYRICS"].isEmpty()) Decode(map["LYRICS"].front(), codec, song->mutable_lyrics());
|
||||
else if (!map["UNSYNCEDLYRICS"].isEmpty()) Decode(map["UNSYNCEDLYRICS"].front(), codec, song->mutable_lyrics());
|
||||
if (!map["LYRICS"].isEmpty()) Decode(map["LYRICS"].front(), song->mutable_lyrics());
|
||||
else if (!map["UNSYNCEDLYRICS"].isEmpty()) Decode(map["UNSYNCEDLYRICS"].front(), song->mutable_lyrics());
|
||||
|
||||
}
|
||||
|
||||
void TagReader::ParseAPETag(const TagLib::APE::ItemListMap &map, const QTextCodec *codec, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const {
|
||||
|
||||
Q_UNUSED(codec);
|
||||
void TagReader::ParseAPETag(const TagLib::APE::ItemListMap &map, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const {
|
||||
|
||||
TagLib::APE::ItemListMap::ConstIterator it = map.find("ALBUM ARTIST");
|
||||
if (it != map.end()) {
|
||||
TagLib::StringList album_artists = it->second.values();
|
||||
if (!album_artists.isEmpty()) {
|
||||
Decode(album_artists.front(), nullptr, song->mutable_albumartist());
|
||||
Decode(album_artists.front(), song->mutable_albumartist());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,19 +479,19 @@ void TagReader::ParseAPETag(const TagLib::APE::ItemListMap &map, const QTextCode
|
||||
}
|
||||
|
||||
if (map.contains("PERFORMER")) {
|
||||
Decode(map["PERFORMER"].values().toString(", "), nullptr, song->mutable_performer());
|
||||
Decode(map["PERFORMER"].values().toString(", "), song->mutable_performer());
|
||||
}
|
||||
|
||||
if (map.contains("COMPOSER")) {
|
||||
Decode(map["COMPOSER"].values().toString(", "), nullptr, song->mutable_composer());
|
||||
Decode(map["COMPOSER"].values().toString(", "), song->mutable_composer());
|
||||
}
|
||||
|
||||
if (map.contains("GROUPING")) {
|
||||
Decode(map["GROUPING"].values().toString(" "), nullptr, song->mutable_grouping());
|
||||
Decode(map["GROUPING"].values().toString(" "), song->mutable_grouping());
|
||||
}
|
||||
|
||||
if (map.contains("LYRICS")) {
|
||||
Decode(map["LYRICS"].toString(), nullptr, song->mutable_lyrics());
|
||||
Decode(map["LYRICS"].toString(), song->mutable_lyrics());
|
||||
}
|
||||
|
||||
if (map.contains("FMPS_PLAYCOUNT")) {
|
||||
@@ -517,8 +508,8 @@ void TagReader::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, con
|
||||
vorbis_comments->addField("COMPOSER", StdStringToTaglibString(song.composer()), true);
|
||||
vorbis_comments->addField("PERFORMER", StdStringToTaglibString(song.performer()), true);
|
||||
vorbis_comments->addField("CONTENT GROUP", StdStringToTaglibString(song.grouping()), true);
|
||||
vorbis_comments->addField("DISCNUMBER", QStringToTaglibString(song.disc() <= 0 -1 ? QString() : QString::number(song.disc())), true);
|
||||
vorbis_comments->addField("COMPILATION", StdStringToTaglibString(song.compilation() ? "1" : "0"), true);
|
||||
vorbis_comments->addField("DISCNUMBER", QStringToTaglibString(song.disc() <= 0 ? QString() : QString::number(song.disc())), true);
|
||||
vorbis_comments->addField("COMPILATION", QStringToTaglibString(song.compilation() ? "1" : QString()), true);
|
||||
|
||||
// Try to be coherent, the two forms are used but the first one is preferred
|
||||
|
||||
@@ -538,13 +529,16 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad
|
||||
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));;
|
||||
if (!fileref || fileref->isNull()) return false;
|
||||
|
||||
fileref->tag()->setTitle(StdStringToTaglibString(song.title()));
|
||||
fileref->tag()->setArtist(StdStringToTaglibString(song.artist()));
|
||||
fileref->tag()->setAlbum(StdStringToTaglibString(song.album()));
|
||||
fileref->tag()->setGenre(StdStringToTaglibString(song.genre()));
|
||||
fileref->tag()->setComment(StdStringToTaglibString(song.comment()));
|
||||
fileref->tag()->setYear(song.year());
|
||||
fileref->tag()->setTrack(song.track());
|
||||
fileref->tag()->setTitle(song.title().empty() ? TagLib::String() : StdStringToTaglibString(song.title()));
|
||||
fileref->tag()->setArtist(song.artist().empty() ? TagLib::String() : StdStringToTaglibString(song.artist()));
|
||||
fileref->tag()->setAlbum(song.album().empty() ? TagLib::String() : StdStringToTaglibString(song.album()));
|
||||
fileref->tag()->setGenre(song.genre().empty() ? TagLib::String() : StdStringToTaglibString(song.genre()));
|
||||
fileref->tag()->setComment(song.comment().empty() ? TagLib::String() : StdStringToTaglibString(song.comment()));
|
||||
fileref->tag()->setYear(song.year() <= 0 ? 0 : song.year());
|
||||
fileref->tag()->setTrack(song.track() <= 0 ? 0 : song.track());
|
||||
|
||||
bool saved = false;
|
||||
bool result = false;
|
||||
|
||||
if (TagLib::FLAC::File *file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
|
||||
TagLib::Ogg::XiphComment *tag = file->xiphComment();
|
||||
@@ -572,14 +566,16 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad
|
||||
else if (TagLib::MPEG::File *file_mpeg = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
|
||||
TagLib::ID3v2::Tag *tag = file_mpeg->ID3v2Tag(true);
|
||||
if (!tag) return false;
|
||||
SetTextFrame("TPOS", song.disc() <= 0 -1 ? QString() : QString::number(song.disc()), tag);
|
||||
SetTextFrame("TCOM", song.composer(), tag);
|
||||
SetTextFrame("TIT1", song.grouping(), tag);
|
||||
SetTextFrame("TOPE", song.performer(), tag);
|
||||
SetTextFrame("TPOS", song.disc() <= 0 ? QString() : QString::number(song.disc()), tag);
|
||||
SetTextFrame("TCOM", song.composer().empty() ? std::string() : song.composer(), tag);
|
||||
SetTextFrame("TIT1", song.grouping().empty() ? std::string() : song.grouping(), tag);
|
||||
SetTextFrame("TOPE", song.performer().empty() ? std::string() : song.performer(), tag);
|
||||
// Skip TPE1 (which is the artist) here because we already set it
|
||||
SetTextFrame("TPE2", song.albumartist(), tag);
|
||||
SetTextFrame("TCMP", std::string(song.compilation() ? "1" : "0"), tag);
|
||||
SetUnsyncLyricsFrame(song.lyrics(), tag);
|
||||
SetTextFrame("TPE2", song.albumartist().empty() ? std::string() : song.albumartist(), tag);
|
||||
SetTextFrame("TCMP", song.compilation() ? QString::number(1) : QString(), tag);
|
||||
SetUnsyncLyricsFrame(song.lyrics().empty() ? std::string() : song.lyrics(), tag);
|
||||
result = file_mpeg->save(TagLib::MPEG::File::ID3v2);
|
||||
saved = true;
|
||||
}
|
||||
|
||||
else if (TagLib::MP4::File *file_mp4 = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
|
||||
@@ -598,53 +594,29 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad
|
||||
SetVorbisComments(tag, song);
|
||||
}
|
||||
|
||||
bool ret = fileref->save();
|
||||
if (!saved) {
|
||||
result = fileref->save();
|
||||
}
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
if (ret) {
|
||||
if (result) {
|
||||
// Linux: inotify doesn't seem to notice the change to the file unless we change the timestamps as well. (this is what touch does)
|
||||
utimensat(0, QFile::encodeName(filename).constData(), nullptr, 0);
|
||||
}
|
||||
#endif // Q_OS_LINUX
|
||||
|
||||
return ret;
|
||||
return result;
|
||||
}
|
||||
|
||||
void TagReader::SaveAPETag(TagLib::APE::Tag *tag, const pb::tagreader::SongMetadata &song) const {
|
||||
|
||||
tag->setItem("album artist", TagLib::APE::Item("album artist", TagLib::StringList(song.albumartist().c_str())));
|
||||
tag->setItem("disc", TagLib::APE::Item("disc", TagLib::String::number(song.disc() <= 0 - 1 ? 0 : song.disc())));
|
||||
tag->addValue("disc", QStringToTaglibString(song.disc() <= 0 ? QString() : QString::number(song.disc())), true);
|
||||
tag->setItem("composer", TagLib::APE::Item("composer", TagLib::StringList(song.composer().c_str())));
|
||||
tag->setItem("grouping", TagLib::APE::Item("grouping", TagLib::StringList(song.grouping().c_str())));
|
||||
tag->setItem("performer", TagLib::APE::Item("performer", TagLib::StringList(song.performer().c_str())));
|
||||
tag->setItem("lyrics", TagLib::APE::Item("lyrics", TagLib::String(song.lyrics())));
|
||||
tag->setItem("compilation", TagLib::APE::Item("compilation", TagLib::StringList(song.compilation() ? "1" : "0")));
|
||||
|
||||
}
|
||||
|
||||
void TagReader::SetUserTextFrame(const QString &description, const QString &value, TagLib::ID3v2::Tag *tag) const {
|
||||
|
||||
const QByteArray descr_utf8(description.toUtf8());
|
||||
const QByteArray value_utf8(value.toUtf8());
|
||||
qLog(Debug) << "Setting FMPSFrame:" << description << ", " << value;
|
||||
SetUserTextFrame(std::string(descr_utf8.constData(), descr_utf8.length()), std::string(value_utf8.constData(), value_utf8.length()), tag);
|
||||
|
||||
}
|
||||
|
||||
void TagReader::SetUserTextFrame(const std::string &description, const std::string &value, TagLib::ID3v2::Tag *tag) const {
|
||||
|
||||
const TagLib::String t_description = StdStringToTaglibString(description);
|
||||
// Remove the frame if it already exists
|
||||
TagLib::ID3v2::UserTextIdentificationFrame *frame = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, t_description);
|
||||
if (frame) {
|
||||
tag->removeFrame(frame);
|
||||
}
|
||||
|
||||
// Create and add a new frame
|
||||
frame = new TagLib::ID3v2::UserTextIdentificationFrame(TagLib::String::UTF8);
|
||||
|
||||
frame->setDescription(t_description);
|
||||
frame->setText(StdStringToTaglibString(value));
|
||||
tag->addFrame(frame);
|
||||
tag->addValue("compilation", QStringToTaglibString(song.compilation() ? QString::number(1) : QString()), true);
|
||||
|
||||
}
|
||||
|
||||
@@ -652,6 +624,7 @@ void TagReader::SetTextFrame(const char *id, const QString &value, TagLib::ID3v2
|
||||
|
||||
const QByteArray utf8(value.toUtf8());
|
||||
SetTextFrame(id, std::string(utf8.constData(), utf8.length()), tag);
|
||||
|
||||
}
|
||||
|
||||
void TagReader::SetTextFrame(const char *id, const std::string &value, TagLib::ID3v2::Tag *tag) const {
|
||||
@@ -665,6 +638,8 @@ void TagReader::SetTextFrame(const char *id, const std::string &value, TagLib::I
|
||||
tag->removeFrame(tag->frameListMap()[id_vector].front());
|
||||
}
|
||||
|
||||
if (value.empty()) return;
|
||||
|
||||
// If no frames stored create empty frame
|
||||
if (frames_buffer.isEmpty()) {
|
||||
TagLib::ID3v2::TextIdentificationFrame frame(id_vector, TagLib::String::UTF8);
|
||||
@@ -672,9 +647,9 @@ void TagReader::SetTextFrame(const char *id, const std::string &value, TagLib::I
|
||||
}
|
||||
|
||||
// Update and add the frames
|
||||
for (int lyrics_index = 0; lyrics_index < frames_buffer.size(); lyrics_index++) {
|
||||
TagLib::ID3v2::TextIdentificationFrame* frame = new TagLib::ID3v2::TextIdentificationFrame(frames_buffer.at(lyrics_index));
|
||||
if (lyrics_index == 0) {
|
||||
for (int i = 0 ; i < frames_buffer.size() ; ++i) {
|
||||
TagLib::ID3v2::TextIdentificationFrame *frame = new TagLib::ID3v2::TextIdentificationFrame(frames_buffer.at(i));
|
||||
if (i == 0) {
|
||||
frame->setText(StdStringToTaglibString(value));
|
||||
}
|
||||
// add frame takes ownership and clears the memory
|
||||
@@ -683,15 +658,6 @@ void TagReader::SetTextFrame(const char *id, const std::string &value, TagLib::I
|
||||
|
||||
}
|
||||
|
||||
bool TagReader::IsMediaFile(const QString &filename) const {
|
||||
|
||||
qLog(Debug) << "Checking for valid file" << filename;
|
||||
|
||||
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
||||
return !fileref->isNull() && fileref->tag();
|
||||
|
||||
}
|
||||
|
||||
QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const {
|
||||
|
||||
if (filename.isEmpty()) return QByteArray();
|
||||
@@ -711,8 +677,7 @@ QByteArray TagReader::LoadEmbeddedArt(const QString &filename) const {
|
||||
if (flac_file && flac_file->xiphComment()) {
|
||||
TagLib::List<TagLib::FLAC::Picture*> pics = flac_file->pictureList();
|
||||
if (!pics.isEmpty()) {
|
||||
// Use the first picture in the file - this could be made cleverer and
|
||||
// pick the front cover if it's present.
|
||||
// Use the first picture in the file - this could be made cleverer and pick the front cover if it's present.
|
||||
|
||||
std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin();
|
||||
TagLib::FLAC::Picture *picture = *it;
|
||||
@@ -817,7 +782,7 @@ QByteArray TagReader::LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) co
|
||||
|
||||
}
|
||||
|
||||
void TagReader::SetUnsyncLyricsFrame(const std::string& value, TagLib::ID3v2::Tag* tag) const {
|
||||
void TagReader::SetUnsyncLyricsFrame(const std::string &value, TagLib::ID3v2::Tag *tag) const {
|
||||
|
||||
TagLib::ByteVector id_vector("USLT");
|
||||
QVector<TagLib::ByteVector> frames_buffer;
|
||||
@@ -828,6 +793,8 @@ void TagReader::SetUnsyncLyricsFrame(const std::string& value, TagLib::ID3v2::Ta
|
||||
tag->removeFrame(tag->frameListMap()[id_vector].front());
|
||||
}
|
||||
|
||||
if (value.empty()) return;
|
||||
|
||||
// If no frames stored create empty frame
|
||||
if (frames_buffer.isEmpty()) {
|
||||
TagLib::ID3v2::UnsynchronizedLyricsFrame frame(TagLib::String::UTF8);
|
||||
@@ -836,9 +803,9 @@ void TagReader::SetUnsyncLyricsFrame(const std::string& value, TagLib::ID3v2::Ta
|
||||
}
|
||||
|
||||
// Update and add the frames
|
||||
for (int lyrics_index = 0; lyrics_index < frames_buffer.size(); lyrics_index++) {
|
||||
TagLib::ID3v2::UnsynchronizedLyricsFrame* frame = new TagLib::ID3v2::UnsynchronizedLyricsFrame(frames_buffer.at(lyrics_index));
|
||||
if (lyrics_index == 0) {
|
||||
for (int i = 0 ; i < frames_buffer.size() ; ++i) {
|
||||
TagLib::ID3v2::UnsynchronizedLyricsFrame *frame = new TagLib::ID3v2::UnsynchronizedLyricsFrame(frames_buffer.at(i));
|
||||
if (i == 0) {
|
||||
frame->setText(StdStringToTaglibString(value));
|
||||
}
|
||||
// add frame takes ownership and clears the memory
|
||||
|
||||
@@ -35,8 +35,6 @@
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
class QTextCodec;
|
||||
|
||||
#ifndef USE_SYSTEM_TAGLIB
|
||||
using namespace Strawberry_TagLib;
|
||||
#endif
|
||||
@@ -52,27 +50,24 @@ class TagReader {
|
||||
explicit TagReader();
|
||||
~TagReader();
|
||||
|
||||
bool IsMediaFile(const QString &filename) const;
|
||||
pb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const;
|
||||
|
||||
void ReadFile(const QString &filename, pb::tagreader::SongMetadata *song) const;
|
||||
bool SaveFile(const QString &filename, const pb::tagreader::SongMetadata &song) const;
|
||||
|
||||
bool IsMediaFile(const QString &filename) const;
|
||||
QByteArray LoadEmbeddedArt(const QString &filename) const;
|
||||
QByteArray LoadEmbeddedAPEArt(const TagLib::APE::ItemListMap &map) const;
|
||||
|
||||
static void Decode(const TagLib::String& tag, const QTextCodec *codec, std::string *output);
|
||||
static void Decode(const QString &tag, const QTextCodec *codec, std::string *output);
|
||||
static void Decode(const TagLib::String &tag, std::string *output);
|
||||
static void Decode(const QString &tag, std::string *output);
|
||||
|
||||
void ParseOggTag(const TagLib::Ogg::FieldListMap &map, const QTextCodec *codec, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const;
|
||||
void ParseAPETag(const TagLib::APE::ItemListMap &map, const QTextCodec *codec, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const;
|
||||
void ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const;
|
||||
void ParseAPETag(const TagLib::APE::ItemListMap &map, QString *disc, QString *compilation, pb::tagreader::SongMetadata *song) const;
|
||||
|
||||
void SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, const pb::tagreader::SongMetadata &song) const;
|
||||
void SaveAPETag(TagLib::APE::Tag *tag, const pb::tagreader::SongMetadata &song) const;
|
||||
|
||||
void SetUserTextFrame(const QString &description, const QString &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetUserTextFrame(const std::string &description, const std::string& value, TagLib::ID3v2::Tag *tag) const;
|
||||
|
||||
void SetTextFrame(const char *id, const QString &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetTextFrame(const char *id, const std::string &value, TagLib::ID3v2::Tag *tag) const;
|
||||
void SetUnsyncLyricsFrame(const std::string& value, TagLib::ID3v2::Tag* tag) const;
|
||||
|
||||
@@ -15,8 +15,6 @@ endif()
|
||||
link_directories(
|
||||
${GLIB_LIBRARY_DIRS}
|
||||
${TAGLIB_LIBRARY_DIRS}
|
||||
${QtCore_LIBRARY_DIRS}
|
||||
${QtNetwork_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
add_executable(strawberry-tagreader ${SOURCES} ${MOC} ${QRC})
|
||||
@@ -24,8 +22,6 @@ add_executable(strawberry-tagreader ${SOURCES} ${MOC} ${QRC})
|
||||
target_include_directories(strawberry-tagreader SYSTEM PRIVATE
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
${PROTOBUF_INCLUDE_DIRS}
|
||||
${QtCore_INCLUDE_DIRS}
|
||||
${QtNetwork_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_include_directories(strawberry-tagreader PRIVATE
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
name: strawberry
|
||||
version: '0.8.2+git'
|
||||
adopt-info: strawberry
|
||||
summary: music player and collection organizer
|
||||
description: |
|
||||
Strawberry is a music player and collection organizer.
|
||||
It is a fork of Clementine released in 2018 aimed at music collectors,
|
||||
audio enthusiasts and audiophiles
|
||||
It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles
|
||||
|
||||
grade: stable
|
||||
confinement: strict
|
||||
@@ -43,7 +42,7 @@ parts:
|
||||
|
||||
alsa-lib:
|
||||
plugin: autotools
|
||||
source: https://www.alsa-project.org/files/pub/lib/alsa-lib-1.2.1.2.tar.bz2
|
||||
source: https://www.alsa-project.org/files/pub/lib/alsa-lib-1.2.3.tar.bz2
|
||||
configflags:
|
||||
- --prefix=/usr
|
||||
- --sysconfdir=/etc
|
||||
@@ -71,6 +70,9 @@ parts:
|
||||
after:
|
||||
- alsa-lib
|
||||
- desktop-qt5
|
||||
override-pull: |
|
||||
snapcraftctl pull
|
||||
snapcraftctl set-version "$(git describe --tags | sed 's/^v//' | cut -d "-" -f1-2)"
|
||||
override-build: |
|
||||
cmake ../src -DCMAKE_INSTALL_PREFIX=/usr
|
||||
make -j $(getconf _NPROCESSORS_ONLN)
|
||||
@@ -84,6 +86,7 @@ parts:
|
||||
- gcc
|
||||
- g++
|
||||
- protobuf-compiler
|
||||
- gettext
|
||||
- libglib2.0-dev
|
||||
- libgnutls28-dev
|
||||
- libdbus-1-dev
|
||||
@@ -96,6 +99,7 @@ parts:
|
||||
- qtbase5-dev
|
||||
- qtbase5-dev-tools
|
||||
- qtbase5-private-dev
|
||||
- qttools5-dev
|
||||
- libqt5x11extras5-dev
|
||||
- libgstreamer1.0-dev
|
||||
- libgstreamer-plugins-base1.0-dev
|
||||
@@ -154,6 +158,10 @@ parts:
|
||||
- gstreamer1.0-plugins-bad
|
||||
- gstreamer1.0-plugins-ugly
|
||||
- gstreamer1.0-libav
|
||||
- qt5-gtk-platformtheme
|
||||
- plasma-integration
|
||||
- kde-style-breeze
|
||||
- qtwayland5
|
||||
|
||||
apps:
|
||||
strawberry:
|
||||
@@ -174,7 +182,7 @@ apps:
|
||||
- x11
|
||||
- wayland
|
||||
- alsa
|
||||
- pulseaudio
|
||||
- audio-playback
|
||||
- removable-media
|
||||
- optical-drive
|
||||
- raw-usb
|
||||
|
||||
@@ -184,6 +184,7 @@ set(SOURCES
|
||||
dialogs/userpassdialog.cpp
|
||||
dialogs/deleteconfirmationdialog.cpp
|
||||
dialogs/lastfmimportdialog.cpp
|
||||
dialogs/snapdialog.cpp
|
||||
|
||||
widgets/autoexpandingtreeview.cpp
|
||||
widgets/busyindicator.cpp
|
||||
@@ -399,6 +400,7 @@ set(HEADERS
|
||||
dialogs/userpassdialog.h
|
||||
dialogs/deleteconfirmationdialog.h
|
||||
dialogs/lastfmimportdialog.h
|
||||
dialogs/snapdialog.h
|
||||
|
||||
widgets/autoexpandingtreeview.h
|
||||
widgets/busyindicator.h
|
||||
@@ -513,6 +515,7 @@ set(UI
|
||||
dialogs/addstreamdialog.ui
|
||||
dialogs/userpassdialog.ui
|
||||
dialogs/lastfmimportdialog.ui
|
||||
dialogs/snapdialog.ui
|
||||
|
||||
widgets/trackslider.ui
|
||||
widgets/fileview.ui
|
||||
@@ -556,8 +559,8 @@ if(HAVE_GLOBALSHORTCUTS)
|
||||
SOURCES globalshortcuts/globalshortcut-x11.cpp
|
||||
)
|
||||
optional_source(HAVE_DBUS
|
||||
SOURCES globalshortcuts/globalshortcutbackend-gsd.cpp
|
||||
HEADERS globalshortcuts/globalshortcutbackend-gsd.h
|
||||
SOURCES globalshortcuts/globalshortcutbackend-gsd.cpp globalshortcuts/globalshortcutbackend-kde.cpp
|
||||
HEADERS globalshortcuts/globalshortcutbackend-gsd.h globalshortcuts/globalshortcutbackend-kde.h
|
||||
)
|
||||
optional_source(WIN32
|
||||
SOURCES globalshortcuts/globalshortcut-win.cpp
|
||||
@@ -631,6 +634,14 @@ if(UNIX AND HAVE_DBUS)
|
||||
dbus/org.gnome.SettingsDaemon.MediaKeys.xml
|
||||
dbus/gnomesettingsdaemon)
|
||||
|
||||
# org.kde.KGlobalAccel interface
|
||||
qt6_add_dbus_interface(SOURCES
|
||||
dbus/org.kde.KGlobalAccel.xml
|
||||
dbus/kglobalaccel)
|
||||
qt6_add_dbus_interface(SOURCES
|
||||
dbus/org.kde.KGlobalAccel.Component.xml
|
||||
dbus/kglobalaccelcomponent)
|
||||
|
||||
else()
|
||||
|
||||
# MPRIS 2.0 DBUS interfaces
|
||||
@@ -659,6 +670,14 @@ if(UNIX AND HAVE_DBUS)
|
||||
dbus/org.gnome.SettingsDaemon.MediaKeys.xml
|
||||
dbus/gnomesettingsdaemon)
|
||||
|
||||
# org.kde.KGlobalAccel interface
|
||||
qt5_add_dbus_interface(SOURCES
|
||||
dbus/org.kde.KGlobalAccel.xml
|
||||
dbus/kglobalaccel)
|
||||
qt5_add_dbus_interface(SOURCES
|
||||
dbus/org.kde.KGlobalAccel.Component.xml
|
||||
dbus/kglobalaccelcomponent)
|
||||
|
||||
endif()
|
||||
|
||||
# org.freedesktop.Avahi.Server interface
|
||||
@@ -1030,7 +1049,6 @@ link_directories(
|
||||
${GNUTLS_LIBRARY_DIRS}
|
||||
${SQLITE_LIBRARY_DIRS}
|
||||
${TAGLIB_LIBRARY_DIRS}
|
||||
${QT_LIBRARY_DIRS}
|
||||
${SINGLEAPPLICATION_LIBRARY_DIRS}
|
||||
${SINGLECOREAPPLICATION_LIBRARY_DIRS}
|
||||
${QTSPARKLE_LIBRARY_DIRS}
|
||||
@@ -1105,7 +1123,6 @@ target_include_directories(strawberry_lib SYSTEM PUBLIC
|
||||
${GOBJECT_INCLUDE_DIRS}
|
||||
${GNUTLS_INCLUDE_DIRS}
|
||||
${SQLITE_INCLUDE_DIRS}
|
||||
${QT_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_include_directories(strawberry_lib PUBLIC
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
// INSTRUCTIONS Base2D
|
||||
// 1. do anything that depends on height() in init(), Base2D will call it before you are shown
|
||||
// 2. otherwise you can use the constructor to initialise things
|
||||
// 2. otherwise you can use the constructor to initialize things
|
||||
// 3. reimplement analyze(), and paint to canvas(), Base2D will update the widget when you return control to it
|
||||
// 4. if you want to manipulate the scope, reimplement transform()
|
||||
// 5. for convenience <vector> <qpixmap.h> <qwdiget.h> are pre-included
|
||||
|
||||
@@ -136,7 +136,7 @@ void AnalyzerContainer::ChangeAnalyzer(int id) {
|
||||
QObject *instance = analyzer_types_[id]->newInstance(Q_ARG(QWidget*, this));
|
||||
|
||||
if (!instance) {
|
||||
qLog(Warning) << "Couldn't initialise a new" << analyzer_types_[id]->className();
|
||||
qLog(Warning) << "Couldn't initialize a new" << analyzer_types_[id]->className();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ class RainbowAnalyzer : public Analyzer::Base {
|
||||
}
|
||||
|
||||
private:
|
||||
// "constants" that get initialised in the constructor
|
||||
// "constants" that get initialized in the constructor
|
||||
float band_scale_[kRainbowBands];
|
||||
QPen colors_[kRainbowBands];
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
#include <QMap>
|
||||
#include <QMetaType>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
@@ -817,7 +818,7 @@ CollectionModel::QueryResult CollectionModel::RunQuery(CollectionItem *parent) {
|
||||
int child_level = parent == root_ ? 0 : parent->container_level + 1;
|
||||
GroupBy child_type = child_level >= 3 ? GroupBy_None : group_by_[child_level];
|
||||
|
||||
// Initialise the query. child_type says what type of thing we want (artists, songs, etc.)
|
||||
// Initialize the query. child_type says what type of thing we want (artists, songs, etc.)
|
||||
CollectionQuery q(query_options_);
|
||||
InitQuery(child_type, &q);
|
||||
|
||||
@@ -892,7 +893,11 @@ void CollectionModel::LazyPopulate(CollectionItem *parent, const bool signal) {
|
||||
}
|
||||
|
||||
void CollectionModel::ResetAsync() {
|
||||
QFuture<CollectionModel::QueryResult> future = QtConcurrent::run(std::bind(&CollectionModel::RunQuery, this, root_));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QFuture<CollectionModel::QueryResult> future = QtConcurrent::run(&CollectionModel::RunQuery, this, root_);
|
||||
#else
|
||||
QFuture<CollectionModel::QueryResult> future = QtConcurrent::run(this, &CollectionModel::RunQuery, root_);
|
||||
#endif
|
||||
NewClosure(future, this, SLOT(ResetAsyncQueryFinished(QFuture<CollectionModel::QueryResult>)), future);
|
||||
}
|
||||
|
||||
@@ -1133,7 +1138,7 @@ CollectionItem *CollectionModel::InitItem(const GroupBy type, const bool signal,
|
||||
|
||||
if (signal) beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count());
|
||||
|
||||
// Initialise the item depending on what type it's meant to be
|
||||
// Initialize the item depending on what type it's meant to be
|
||||
CollectionItem *item = new CollectionItem(item_type, parent);
|
||||
item->compilation_artist_node_ = nullptr;
|
||||
item->container_level = container_level;
|
||||
@@ -1566,8 +1571,9 @@ void CollectionModel::FinishItem(const GroupBy type, const bool signal, const bo
|
||||
// Create the divider entry if we're supposed to
|
||||
if (create_divider && show_dividers_) {
|
||||
QString divider_key = DividerKey(type, item);
|
||||
if (item->sort_text.isEmpty() || item->sort_text[0].toLower() != divider_key)
|
||||
item->sort_text.prepend(divider_key);
|
||||
if (!divider_key.isEmpty()) {
|
||||
item->sort_text.prepend(divider_key + " ");
|
||||
}
|
||||
|
||||
if (!divider_key.isEmpty() && !divider_nodes_.contains(divider_key)) {
|
||||
if (signal)
|
||||
@@ -1576,7 +1582,7 @@ void CollectionModel::FinishItem(const GroupBy type, const bool signal, const bo
|
||||
CollectionItem *divider = new CollectionItem(CollectionItem::Type_Divider, root_);
|
||||
divider->key = divider_key;
|
||||
divider->display_text = DividerDisplayText(type, divider_key);
|
||||
divider->sort_text = divider_key + "0000";
|
||||
divider->sort_text = divider_key + " ";
|
||||
divider->lazy_loaded = true;
|
||||
|
||||
divider_nodes_[divider_key] = divider;
|
||||
@@ -1730,8 +1736,14 @@ bool CollectionModel::CompareItems(const CollectionItem *a, const CollectionItem
|
||||
QVariant left(data(a, CollectionModel::Role_SortText));
|
||||
QVariant right(data(b, CollectionModel::Role_SortText));
|
||||
|
||||
if (left.type() == QVariant::Int) return left.toInt() < right.toInt();
|
||||
return left.toString() < right.toString();
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
if (left.metaType().id() == QMetaType::Int)
|
||||
#else
|
||||
if (left.type() == QVariant::Int)
|
||||
#endif
|
||||
return left.toInt() < right.toInt();
|
||||
else
|
||||
return left.toString() < right.toString();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QMetaType>
|
||||
#include <QDateTime>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
@@ -133,9 +134,23 @@ void CollectionQuery::AddWhere(const QString &column, const QVariant &value, con
|
||||
}
|
||||
else {
|
||||
// Do integers inline - sqlite seems to get confused when you pass integers to bound parameters
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
if (value.metaType().id() == QMetaType::Int) {
|
||||
#else
|
||||
if (value.type() == QVariant::Int) {
|
||||
#endif
|
||||
where_clauses_ << QString("%1 %2 %3").arg(column, op, value.toString());
|
||||
}
|
||||
else if (
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
value.metaType().id() == QMetaType::QString
|
||||
#else
|
||||
value.type() == QVariant::String
|
||||
#endif
|
||||
&& value.toString().isNull()) {
|
||||
where_clauses_ << QString("%1 %2 ?").arg(column, op);
|
||||
bound_values_ << QString("");
|
||||
}
|
||||
else {
|
||||
where_clauses_ << QString("%1 %2 ?").arg(column, op);
|
||||
bound_values_ << value;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <QtGlobal>
|
||||
#include <QMutex>
|
||||
#include <QMimeData>
|
||||
#include <QMetaType>
|
||||
#include <QVariant>
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
@@ -444,8 +445,13 @@ bool ContextAlbumsModel::CompareItems(const CollectionItem *a, const CollectionI
|
||||
QVariant left(data(a, ContextAlbumsModel::Role_SortText));
|
||||
QVariant right(data(b, ContextAlbumsModel::Role_SortText));
|
||||
|
||||
if (left.type() == QVariant::Int) return left.toInt() < right.toInt();
|
||||
return left.toString() < right.toString();
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
if (left.metaType().id() == QMetaType::Int)
|
||||
#else
|
||||
if (left.type() == QVariant::Int)
|
||||
#endif
|
||||
return left.toInt() < right.toInt();
|
||||
else return left.toString() < right.toString();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -397,7 +397,7 @@ void ContextAlbumsView::EditTracks() {
|
||||
void ContextAlbumsView::CopyToDevice() {
|
||||
#ifndef Q_OS_WIN
|
||||
if (!organize_dialog_)
|
||||
organize_dialog_.reset(new OrganizeDialog(app_->task_manager()));
|
||||
organize_dialog_.reset(new OrganizeDialog(app_->task_manager(), nullptr, this));
|
||||
|
||||
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
|
||||
organize_dialog_->SetCopy(true);
|
||||
|
||||
@@ -151,6 +151,7 @@ ContextView::ContextView(QWidget *parent) :
|
||||
label_top_->setWordWrap(true);
|
||||
label_top_->setMinimumHeight(50);
|
||||
label_top_->setContentsMargins(0, 0, 32, 0);
|
||||
label_top_->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
|
||||
layout_scrollarea_->setObjectName("context-layout-scrollarea");
|
||||
layout_scrollarea_->setContentsMargins(15, 15, 15, 15);
|
||||
|
||||
@@ -132,6 +132,7 @@ QSqlDatabase Database::Connect() {
|
||||
if (db.isOpen()) {
|
||||
return db;
|
||||
}
|
||||
db.setConnectOptions("QSQLITE_BUSY_TIMEOUT=30000");
|
||||
//qLog(Debug) << "Opened database with connection id" << connection_id;
|
||||
|
||||
if (!injected_database_name_.isNull())
|
||||
@@ -202,7 +203,7 @@ QSqlDatabase Database::Connect() {
|
||||
UpdateMainSchema(&db);
|
||||
}
|
||||
|
||||
// We might have to initialise the schema in some attached databases now, if they were deleted and don't match up with the main schema version.
|
||||
// We might have to initialize the schema in some attached databases now, if they were deleted and don't match up with the main schema version.
|
||||
for (const QString &key : attached_databases_.keys()) {
|
||||
if (attached_databases_[key].is_temporary_ && attached_databases_[key].schema_.isEmpty())
|
||||
continue;
|
||||
@@ -518,11 +519,13 @@ void Database::DoBackup() {
|
||||
|
||||
QSqlDatabase db(this->Connect());
|
||||
|
||||
if (!db.isOpen()) return;
|
||||
|
||||
// Before we overwrite anything, make sure the database is not corrupt
|
||||
QMutexLocker l(&mutex_);
|
||||
const bool ok = IntegrityCheck(db);
|
||||
|
||||
if (ok) {
|
||||
const bool ok = IntegrityCheck(db);
|
||||
if (ok && SchemaVersion(&db) == kSchemaVersion) {
|
||||
BackupFile(db.databaseName());
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ protected:
|
||||
|
||||
private:
|
||||
QPixmap normal_icon_;
|
||||
QPixmap grey_icon_;
|
||||
std::unique_ptr<MacSystemTrayIconPrivate> p_;
|
||||
Q_DISABLE_COPY(MacSystemTrayIcon);
|
||||
};
|
||||
|
||||
@@ -165,7 +165,8 @@ class MacSystemTrayIconPrivate {
|
||||
|
||||
MacSystemTrayIcon::MacSystemTrayIcon(QObject* parent)
|
||||
: SystemTrayIcon(parent),
|
||||
normal_icon_(QPixmap(":/pictures/strawberry.png").scaled(128, 128, Qt::KeepAspectRatio, Qt::SmoothTransformation)) {
|
||||
normal_icon_(QPixmap(":/pictures/strawberry.png").scaled(128, 128, Qt::KeepAspectRatio, Qt::SmoothTransformation)),
|
||||
grey_icon_(QPixmap(":/pictures/strawberry-grey.png").scaled(128, 128, Qt::KeepAspectRatio, Qt::SmoothTransformation)) {
|
||||
QApplication::setWindowIcon(normal_icon_);
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
#include "dialogs/addstreamdialog.h"
|
||||
#include "dialogs/deleteconfirmationdialog.h"
|
||||
#include "dialogs/lastfmimportdialog.h"
|
||||
#include "dialogs/snapdialog.h"
|
||||
#include "organize/organizedialog.h"
|
||||
#include "widgets/fancytabwidget.h"
|
||||
#include "widgets/playingwidget.h"
|
||||
@@ -320,7 +321,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
connect(app, SIGNAL(ErrorAdded(QString)), SLOT(ShowErrorDialog(QString)));
|
||||
connect(app, SIGNAL(SettingsDialogRequested(SettingsDialog::Page)), SLOT(OpenSettingsDialogAtPage(SettingsDialog::Page)));
|
||||
|
||||
// Initialise the UI
|
||||
// Initialize the UI
|
||||
ui_->setupUi(this);
|
||||
|
||||
album_cover_choice_controller_->Init(app);
|
||||
@@ -329,7 +330,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
context_view_->Init(app_, collection_view_->view(), album_cover_choice_controller_);
|
||||
ui_->widget_playing->Init(app_, album_cover_choice_controller_);
|
||||
|
||||
// Initialise the search widget
|
||||
// Initialize the search widget
|
||||
StyleHelper::setBaseColor(palette().color(QPalette::Highlight).darker());
|
||||
|
||||
// Add tabs to the fancy tab widget
|
||||
@@ -362,8 +363,8 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
track_slider_timer_->setInterval(kTrackSliderUpdateTimeMs);
|
||||
connect(track_slider_timer_, SIGNAL(timeout()), SLOT(UpdateTrackSliderPosition()));
|
||||
|
||||
// Start initialising the player
|
||||
qLog(Debug) << "Initialising player";
|
||||
// Start initializing the player
|
||||
qLog(Debug) << "Initializing player";
|
||||
app_->player()->SetAnalyzer(ui_->analyzer);
|
||||
app_->player()->SetEqualizer(equalizer_.get());
|
||||
app_->player()->Init();
|
||||
@@ -986,6 +987,18 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
if (!Utilities::GetEnv("SNAP").isEmpty() && !Utilities::GetEnv("SNAP_NAME").isEmpty()) {
|
||||
s.beginGroup(kSettingsGroup);
|
||||
if (!s.value("ignore_snap", false).toBool()) {
|
||||
SnapDialog *snap_dialog = new SnapDialog();
|
||||
snap_dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
snap_dialog->show();
|
||||
}
|
||||
s.endGroup();
|
||||
}
|
||||
#endif
|
||||
|
||||
qLog(Debug) << "Started" << QThread::currentThread();
|
||||
initialized_ = true;
|
||||
|
||||
@@ -1010,6 +1023,7 @@ void MainWindow::ReloadSettings() {
|
||||
s.beginGroup(BehaviourSettingsPage::kSettingsGroup);
|
||||
keep_running_ = s.value("keeprunning", false).toBool();
|
||||
playing_widget_ = s.value("playing_widget", true).toBool();
|
||||
bool trayicon_progress = s.value("trayicon_progress", false).toBool();
|
||||
if (playing_widget_ != ui_->widget_playing->IsEnabled()) TabSwitched();
|
||||
doubleclick_addmode_ = BehaviourSettingsPage::AddBehaviour(s.value("doubleclick_addmode", BehaviourSettingsPage::AddBehaviour_Append).toInt());
|
||||
doubleclick_playmode_ = BehaviourSettingsPage::PlayBehaviour(s.value("doubleclick_playmode", BehaviourSettingsPage::PlayBehaviour_Never).toInt());
|
||||
@@ -1021,6 +1035,8 @@ void MainWindow::ReloadSettings() {
|
||||
int iconsize = s.value(AppearanceSettingsPage::kIconSizePlayControlButtons, 32).toInt();
|
||||
s.endGroup();
|
||||
|
||||
if (tray_icon_) tray_icon_->SetTrayiconProgress(trayicon_progress);
|
||||
|
||||
ui_->back_button->setIconSize(QSize(iconsize, iconsize));
|
||||
ui_->pause_play_button->setIconSize(QSize(iconsize, iconsize));
|
||||
ui_->stop_button->setIconSize(QSize(iconsize, iconsize));
|
||||
@@ -1517,19 +1533,25 @@ void MainWindow::showEvent(QShowEvent *e) {
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *e) {
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
Exit();
|
||||
#else
|
||||
if (!hidden_ && keep_running_ && e->spontaneous() && QSystemTrayIcon::isSystemTrayAvailable()) {
|
||||
e->ignore();
|
||||
SetHiddenInTray(true);
|
||||
}
|
||||
else {
|
||||
Exit();
|
||||
}
|
||||
#endif
|
||||
|
||||
QMainWindow::closeEvent(e);
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::SetHiddenInTray(const bool hidden) {
|
||||
|
||||
hidden_ = hidden;
|
||||
settings_.setValue("hidden", hidden_);
|
||||
|
||||
// Some window managers don't remember maximized state between calls to hide() and show(), so we have to remember it ourself.
|
||||
if (hidden) {
|
||||
@@ -2518,9 +2540,9 @@ void MainWindow::PlaylistCopyUrl() {
|
||||
}
|
||||
|
||||
if (urls.count() > 0) {
|
||||
QMimeData *mime_data = new QMimeData;
|
||||
mime_data->setUrls(urls);
|
||||
QApplication::clipboard()->setMimeData(mime_data);
|
||||
QMimeData mime_data;
|
||||
mime_data.setUrls(urls);
|
||||
QApplication::clipboard()->setText(mime_data.text());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2730,25 +2752,21 @@ void MainWindow::Raise() {
|
||||
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr *result) {
|
||||
#else
|
||||
bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, long *result) {
|
||||
#endif
|
||||
|
||||
Q_UNUSED(eventType);
|
||||
Q_UNUSED(result);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
MSG *msg = static_cast<MSG*>(message);
|
||||
thumbbar_->HandleWinEvent(msg);
|
||||
#else
|
||||
Q_UNUSED(message);
|
||||
#endif
|
||||
|
||||
return false;
|
||||
if (exit_count_ == 0 && message) {
|
||||
MSG *msg = static_cast<MSG*>(message);
|
||||
thumbbar_->HandleWinEvent(msg);
|
||||
}
|
||||
return QMainWindow::nativeEvent(eventType, message, result);
|
||||
|
||||
}
|
||||
#endif // Q_OS_WIN
|
||||
|
||||
void MainWindow::AutoCompleteTags() {
|
||||
|
||||
@@ -2945,7 +2963,7 @@ void MainWindow::SetToggleScrobblingIcon(const bool value) {
|
||||
if (value) {
|
||||
if (app_->playlist_manager()->active() && app_->playlist_manager()->active()->scrobbled())
|
||||
ui_->action_toggle_scrobbling->setIcon(IconLoader::Load("scrobble", 22));
|
||||
else
|
||||
else
|
||||
ui_->action_toggle_scrobbling->setIcon(IconLoader::Load("scrobble", 22)); // TODO: Create a faint version of the icon
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -117,10 +117,12 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
void showEvent(QShowEvent *e) override;
|
||||
void closeEvent(QCloseEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
#ifdef Q_OS_WIN
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;
|
||||
#else
|
||||
bool nativeEvent(const QByteArray &eventType, void *message, long *result) override;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// PlatformInterface
|
||||
|
||||
@@ -69,6 +69,20 @@ int MultiSortFilterProxy::Compare(const QVariant &left, const QVariant &right) c
|
||||
|
||||
// Copied from the QSortFilterProxyModel::lessThan implementation, but returns -1, 0 or 1 instead of true or false.
|
||||
switch (left.userType()) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
case QMetaType::UnknownType: return (right.metaType().id() != QMetaType::UnknownType) ? -1 : 0;
|
||||
case QMetaType::Int: return DoCompare(left.toInt(), right.toInt());
|
||||
case QMetaType::UInt: return DoCompare(left.toUInt(), right.toUInt());
|
||||
case QMetaType::LongLong: return DoCompare(left.toLongLong(), right.toLongLong());
|
||||
case QMetaType::ULongLong: return DoCompare(left.toULongLong(), right.toULongLong());
|
||||
case QMetaType::Float: return DoCompare(left.toFloat(), right.toFloat());
|
||||
case QMetaType::Double: return DoCompare(left.toDouble(), right.toDouble());
|
||||
case QMetaType::Char: return DoCompare(left.toChar(), right.toChar());
|
||||
case QMetaType::QDate: return DoCompare(left.toDate(), right.toDate());
|
||||
case QMetaType::QTime: return DoCompare(left.toTime(), right.toTime());
|
||||
case QMetaType::QDateTime: return DoCompare(left.toDateTime(), right.toDateTime());
|
||||
case QMetaType::QString:
|
||||
#else
|
||||
case QVariant::Invalid: return (right.type() != QVariant::Invalid) ? -1 : 0;
|
||||
case QVariant::Int: return DoCompare(left.toInt(), right.toInt());
|
||||
case QVariant::UInt: return DoCompare(left.toUInt(), right.toUInt());
|
||||
@@ -81,6 +95,7 @@ int MultiSortFilterProxy::Compare(const QVariant &left, const QVariant &right) c
|
||||
case QVariant::Time: return DoCompare(left.toTime(), right.toTime());
|
||||
case QVariant::DateTime: return DoCompare(left.toDateTime(), right.toDateTime());
|
||||
case QVariant::String:
|
||||
#endif
|
||||
default:
|
||||
if (isSortLocaleAware())
|
||||
return left.toString().localeAwareCompare(right.toString());
|
||||
|
||||
@@ -166,7 +166,7 @@ void Player::Init() {
|
||||
CreateEngine(enginetype);
|
||||
}
|
||||
|
||||
if (!engine_->Init()) { qFatal("Error initialising audio engine"); }
|
||||
if (!engine_->Init()) { qFatal("Error initializing audio engine"); }
|
||||
|
||||
analyzer_->SetEngine(engine_.get());
|
||||
|
||||
|
||||
@@ -54,7 +54,10 @@ QtSystemTrayIcon::QtSystemTrayIcon(QObject *parent)
|
||||
action_mute_(nullptr) {
|
||||
|
||||
app_name_[0] = app_name_[0].toUpper();
|
||||
|
||||
QIcon theme_icon_grey = IconLoader::Load("strawberry-grey");
|
||||
if (!theme_icon_grey.isNull()) {
|
||||
grey_icon_ = theme_icon_grey.pixmap(48, QIcon::Disabled);
|
||||
}
|
||||
tray_->setIcon(normal_icon_);
|
||||
tray_->installEventFilter(this);
|
||||
ClearNowPlaying();
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
#include <QUrl>
|
||||
#include <QImage>
|
||||
#include <QIcon>
|
||||
#include <QTextCodec>
|
||||
#include <QSqlQuery>
|
||||
#include <QStandardPaths>
|
||||
#include <QtDebug>
|
||||
@@ -745,14 +744,6 @@ void Song::set_genre_id3(int id) {
|
||||
set_genre(TStringToQString(TagLib::ID3v1::genre(id)));
|
||||
}
|
||||
|
||||
QString Song::Decode(const QString &tag, const QTextCodec *codec) {
|
||||
if (!codec) {
|
||||
return tag;
|
||||
}
|
||||
|
||||
return codec->toUnicode(tag.toUtf8());
|
||||
}
|
||||
|
||||
void Song::InitFromProtobuf(const pb::tagreader::SongMetadata &pb) {
|
||||
|
||||
if (d->source_ == Source_Unknown) d->source_ = Source_LocalFile;
|
||||
@@ -1372,7 +1363,7 @@ void Song::BindToQuery(QSqlQuery *query) const {
|
||||
query->bindValue(":art_automatic", d->art_automatic_.toString(QUrl::FullyEncoded));
|
||||
query->bindValue(":art_manual", d->art_manual_.toString(QUrl::FullyEncoded));
|
||||
|
||||
query->bindValue(":effective_albumartist", this->effective_albumartist());
|
||||
query->bindValue(":effective_albumartist", strval(this->effective_albumartist()));
|
||||
query->bindValue(":effective_originalyear", intval(this->effective_originalyear()));
|
||||
|
||||
query->bindValue(":cue_path", d->cue_path_);
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
#include <QImage>
|
||||
#include <QIcon>
|
||||
|
||||
class QTextCodec;
|
||||
class QSqlQuery;
|
||||
|
||||
namespace Engine {
|
||||
@@ -177,8 +176,6 @@ class Song {
|
||||
// Useful when you want updated tags from disk but you want to keep user stats.
|
||||
void MergeUserSetData(const Song &other);
|
||||
|
||||
static QString Decode(const QString &tag, const QTextCodec *codec = nullptr);
|
||||
|
||||
// Save
|
||||
void BindToQuery(QSqlQuery *query) const;
|
||||
void BindToFtsQuery(QSqlQuery *query) const;
|
||||
|
||||
@@ -439,6 +439,7 @@ SongLoader::Result SongLoader::LoadRemote() {
|
||||
errors_ << tr("Couldn't create gstreamer source element for %1").arg(url_.toString());
|
||||
return Error;
|
||||
}
|
||||
g_object_set(source, "ssl-strict", FALSE, nullptr);
|
||||
|
||||
// Create the other elements and link them up
|
||||
GstElement *typefind = gst_element_factory_make("typefind", nullptr);
|
||||
|
||||
@@ -47,31 +47,31 @@ SystemTrayIcon::SystemTrayIcon(QObject *parent)
|
||||
|
||||
QPixmap SystemTrayIcon::CreateIcon(const QPixmap &icon, const QPixmap &grey_icon) {
|
||||
|
||||
Q_UNUSED(grey_icon);
|
||||
|
||||
QRect rect(icon.rect());
|
||||
|
||||
// The angle of the line that's used to cover the icon.
|
||||
// Centered on rect.topRight()
|
||||
double angle = double(100 - song_progress()) / 100.0 * M_PI_2 + M_PI;
|
||||
double length = sqrt(pow(rect.width(), 2.0) + pow(rect.height(), 2.0));
|
||||
|
||||
QPolygon mask;
|
||||
mask << rect.topRight();
|
||||
mask << rect.topRight() + QPoint(length * sin(angle), -length * cos(angle));
|
||||
|
||||
if (song_progress() > 50) mask << rect.bottomLeft();
|
||||
|
||||
mask << rect.topLeft();
|
||||
mask << rect.topRight();
|
||||
|
||||
QPixmap ret(icon);
|
||||
QPainter p(&ret);
|
||||
|
||||
// Draw the grey bit
|
||||
//p.setClipRegion(mask);
|
||||
//p.drawPixmap(0, 0, grey_icon);
|
||||
//p.setClipping(false);
|
||||
if (trayicon_progress_) {
|
||||
// The angle of the line that's used to cover the icon.
|
||||
// Centered on rect.topLeft()
|
||||
double angle = double(100 - song_progress()) / 100.0 * M_PI_2;
|
||||
double length = sqrt(pow(rect.width(), 2.0) + pow(rect.height(), 2.0));
|
||||
|
||||
QPolygon mask;
|
||||
mask << rect.topLeft();
|
||||
mask << rect.topLeft() + QPoint(length * sin(angle), length * cos(angle));
|
||||
|
||||
if (song_progress() > 50) mask << rect.bottomRight();
|
||||
|
||||
mask << rect.topRight();
|
||||
mask << rect.topLeft();
|
||||
|
||||
// Draw the grey bit
|
||||
p.setClipRegion(mask);
|
||||
p.drawPixmap(0, 0, grey_icon);
|
||||
p.setClipping(false);
|
||||
}
|
||||
|
||||
// Draw the playing or paused icon in the top-right
|
||||
if (!current_state_icon().isNull()) {
|
||||
|
||||
@@ -57,6 +57,7 @@ class SystemTrayIcon : public QObject {
|
||||
|
||||
public slots:
|
||||
void SetProgress(int percentage);
|
||||
void SetTrayiconProgress(bool enabled) { trayicon_progress_ = enabled; }
|
||||
virtual void SetPaused();
|
||||
virtual void SetPlaying(bool enable_play_pause = false);
|
||||
virtual void SetStopped();
|
||||
@@ -85,6 +86,7 @@ class SystemTrayIcon : public QObject {
|
||||
QPixmap playing_icon_;
|
||||
QPixmap paused_icon_;
|
||||
QPixmap current_state_icon_;
|
||||
bool trayicon_progress_;
|
||||
};
|
||||
|
||||
#endif // SYSTEMTRAYICON_H
|
||||
|
||||
@@ -762,29 +762,6 @@ bool IsLaptop() {
|
||||
|
||||
}
|
||||
|
||||
bool UrlOnSameDriveAsStrawberry(const QUrl &url) {
|
||||
|
||||
if (!url.isValid() || !url.isLocalFile() || url.toLocalFile().isEmpty()) return false;
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
QUrl appUrl = QUrl::fromLocalFile(QCoreApplication::applicationDirPath());
|
||||
if (url.toLocalFile().left(1) == appUrl.toLocalFile().left(1))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
#else
|
||||
// Non windows systems have always a / in the path
|
||||
return true;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
QUrl GetRelativePathToStrawberryBin(const QUrl &url) {
|
||||
if (!url.isValid()) return QUrl();
|
||||
QDir appPath(QCoreApplication::applicationDirPath());
|
||||
return QUrl::fromLocalFile(appPath.relativeFilePath(url.toLocalFile()));
|
||||
}
|
||||
|
||||
QString PathWithoutFilenameExtension(const QString &filename) {
|
||||
if (filename.section('/', -1, -1).contains('.')) return filename.section('.', 0, -2);
|
||||
return filename;
|
||||
|
||||
@@ -108,12 +108,6 @@ const char *EnumToString(const QMetaObject &meta, const char *name, int value);
|
||||
QStringList Prepend(const QString &text, const QStringList &list);
|
||||
QStringList Updateify(const QStringList &list);
|
||||
|
||||
// Check if two urls are on the same drive (mainly for windows)
|
||||
bool UrlOnSameDriveAsStrawberry(const QUrl &url);
|
||||
|
||||
// Get relative path to Strawberry binary
|
||||
QUrl GetRelativePathToStrawberryBin(const QUrl &url);
|
||||
|
||||
// Get the path without the filename extension
|
||||
QString PathWithoutFilenameExtension(const QString &filename);
|
||||
QString FiddleFileExtension(const QString &filename, const QString &new_extension);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2020, 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
|
||||
@@ -25,14 +26,17 @@
|
||||
#include <QList>
|
||||
#include <QPixmap>
|
||||
#include <QAction>
|
||||
#include <QTimer>
|
||||
#include <QtDebug>
|
||||
|
||||
#ifndef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0600
|
||||
#endif
|
||||
|
||||
#include <windows.h>
|
||||
#include <commctrl.h>
|
||||
#include <shobjidl.h>
|
||||
|
||||
extern HICON qt_pixmapToWinHICON(const QPixmap &p);
|
||||
|
||||
#include "core/logging.h"
|
||||
@@ -44,10 +48,16 @@ const int Windows7ThumbBar::kMaxButtonCount = 7;
|
||||
Windows7ThumbBar::Windows7ThumbBar(QWidget *widget)
|
||||
: QObject(widget),
|
||||
widget_(widget),
|
||||
button_created_message_id_(0),
|
||||
taskbar_list_(nullptr) {}
|
||||
timer_(new QTimer(this)),
|
||||
button_created_message_id_(0) {
|
||||
|
||||
void Windows7ThumbBar::SetActions(const QList<QAction*>& actions) {
|
||||
timer_->setSingleShot(true);
|
||||
timer_->setInterval(300);
|
||||
connect(timer_, SIGNAL(timeout()), SLOT(ActionChanged()));
|
||||
|
||||
}
|
||||
|
||||
void Windows7ThumbBar::SetActions(const QList<QAction*> &actions) {
|
||||
|
||||
qLog(Debug) << "Setting actions";
|
||||
Q_ASSERT(actions.count() <= kMaxButtonCount);
|
||||
@@ -55,14 +65,39 @@ void Windows7ThumbBar::SetActions(const QList<QAction*>& actions) {
|
||||
actions_ = actions;
|
||||
for (QAction *action : actions) {
|
||||
if (action) {
|
||||
connect(action, SIGNAL(changed()), SLOT(ActionChanged()));
|
||||
connect(action, SIGNAL(changed()), SLOT(ActionChangedTriggered()));
|
||||
}
|
||||
}
|
||||
qLog(Debug) << "Done";
|
||||
|
||||
}
|
||||
|
||||
static void SetupButton(const QAction *action, THUMBBUTTON *button) {
|
||||
ITaskbarList3 *Windows7ThumbBar::CreateTaskbarList() {
|
||||
|
||||
ITaskbarList3 *taskbar_list = nullptr;
|
||||
|
||||
// Copied from win7 SDK shobjidl.h
|
||||
static const GUID CLSID_ITaskbarList = { 0x56FDF344,0xFD6D,0x11d0,{0x95,0x8A,0x00,0x60,0x97,0xC9,0xA0,0x90}};
|
||||
|
||||
// Create the taskbar list
|
||||
HRESULT hr = CoCreateInstance(CLSID_ITaskbarList, nullptr, CLSCTX_ALL, IID_ITaskbarList3, (void**)&taskbar_list);
|
||||
if (hr != S_OK) {
|
||||
qLog(Warning) << "Error creating the ITaskbarList3 interface" << Qt::hex << DWORD (hr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
hr = taskbar_list->HrInit();
|
||||
if (hr != S_OK) {
|
||||
qLog(Warning) << "Error initializing taskbar list" << Qt::hex << DWORD (hr);
|
||||
taskbar_list->Release();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return taskbar_list;
|
||||
|
||||
}
|
||||
|
||||
void Windows7ThumbBar::SetupButton(const QAction *action, THUMBBUTTON *button) {
|
||||
|
||||
if (action) {
|
||||
button->hIcon = qt_pixmapToWinHICON(action->icon().pixmap(Windows7ThumbBar::kIconSize));
|
||||
@@ -77,7 +112,7 @@ static void SetupButton(const QAction *action, THUMBBUTTON *button) {
|
||||
button->dwMask = THUMBBUTTONMASK(THB_ICON | THB_TOOLTIP | THB_FLAGS);
|
||||
}
|
||||
else {
|
||||
button->hIcon = 0;
|
||||
button->hIcon = nullptr;
|
||||
button->szTip[0] = L'\0';
|
||||
button->dwFlags = THBF_NOBACKGROUND;
|
||||
button->dwMask = THUMBBUTTONMASK(THB_FLAGS);
|
||||
@@ -94,51 +129,33 @@ void Windows7ThumbBar::HandleWinEvent(MSG *msg) {
|
||||
}
|
||||
|
||||
if (msg->message == button_created_message_id_) {
|
||||
HRESULT hr;
|
||||
qLog(Debug) << "Button created";
|
||||
// Unref the old taskbar list if we had one
|
||||
if (taskbar_list_) {
|
||||
qLog(Debug) << "Releasing old taskbar list";
|
||||
reinterpret_cast<ITaskbarList3*>(taskbar_list_)->Release();
|
||||
taskbar_list_ = nullptr;
|
||||
}
|
||||
|
||||
// Copied from win7 SDK shobjidl.h
|
||||
static const GUID CLSID_ITaskbarList ={ 0x56FDF344,0xFD6D,0x11d0,{0x95,0x8A,0x00,0x60,0x97,0xC9,0xA0,0x90}};
|
||||
// Create the taskbar list
|
||||
hr = CoCreateInstance(CLSID_ITaskbarList, nullptr, CLSCTX_ALL, IID_ITaskbarList3, (void**)&taskbar_list_);
|
||||
if (hr != S_OK) {
|
||||
qLog(Warning) << "Error creating the ITaskbarList3 interface" << Qt::hex << DWORD (hr);
|
||||
return;
|
||||
}
|
||||
|
||||
ITaskbarList3 *taskbar_list = reinterpret_cast<ITaskbarList3*>(taskbar_list_);
|
||||
hr = taskbar_list->HrInit();
|
||||
if (hr != S_OK) {
|
||||
qLog(Warning) << "Error initialising taskbar list" << Qt::hex << DWORD (hr);
|
||||
taskbar_list->Release();
|
||||
taskbar_list_ = nullptr;
|
||||
return;
|
||||
}
|
||||
ITaskbarList3 *taskbar_list = CreateTaskbarList();
|
||||
if (!taskbar_list) return;
|
||||
|
||||
// Add the buttons
|
||||
qLog(Debug) << "Initialising" << actions_.count() << "buttons";
|
||||
THUMBBUTTON buttons[kMaxButtonCount];
|
||||
for (int i = 0; i < actions_.count(); ++i) {
|
||||
for (int i = 0 ; i < actions_.count() ; ++i) {
|
||||
const QAction *action = actions_[i];
|
||||
THUMBBUTTON *button = &buttons[i];
|
||||
button->iId = i;
|
||||
SetupButton(action, button);
|
||||
}
|
||||
|
||||
qLog(Debug) << "Adding buttons";
|
||||
hr = taskbar_list->ThumbBarAddButtons((HWND)widget_->winId(), actions_.count(), buttons);
|
||||
if (hr != S_OK)
|
||||
qLog(Debug) << "Adding" << actions_.count() << "buttons";
|
||||
HRESULT hr = taskbar_list->ThumbBarAddButtons(reinterpret_cast<HWND>(widget_->winId()), actions_.count(), buttons);
|
||||
if (hr != S_OK) {
|
||||
qLog(Debug) << "Failed to add buttons" << Qt::hex << DWORD (hr);
|
||||
for (int i = 0; i < actions_.count(); i++) {
|
||||
if (buttons[i].hIcon)
|
||||
DestroyIcon (buttons[i].hIcon);
|
||||
}
|
||||
|
||||
for (int i = 0 ; i < actions_.count() ; ++i) {
|
||||
if (buttons[i].hIcon) {
|
||||
DestroyIcon (buttons[i].hIcon);
|
||||
}
|
||||
}
|
||||
|
||||
taskbar_list->Release();
|
||||
|
||||
}
|
||||
else if (msg->message == WM_COMMAND) {
|
||||
const int button_id = LOWORD(msg->wParam);
|
||||
@@ -153,21 +170,38 @@ void Windows7ThumbBar::HandleWinEvent(MSG *msg) {
|
||||
|
||||
}
|
||||
|
||||
void Windows7ThumbBar::ActionChangedTriggered() {
|
||||
if (!timer_->isActive()) timer_->start();
|
||||
}
|
||||
|
||||
void Windows7ThumbBar::ActionChanged() {
|
||||
|
||||
if (!taskbar_list_) return;
|
||||
ITaskbarList3 *taskbar_list = reinterpret_cast<ITaskbarList3*>(taskbar_list_);
|
||||
ITaskbarList3 *taskbar_list = CreateTaskbarList();
|
||||
if (!taskbar_list) return;
|
||||
|
||||
qLog(Debug) << "Updating" << actions_.count() << "buttons";
|
||||
|
||||
THUMBBUTTON buttons[kMaxButtonCount];
|
||||
for (int i = 0; i < actions_.count(); ++i) {
|
||||
const QAction *action = actions_[i];
|
||||
for (int i = 0 ; i < actions_.count() ; ++i) {
|
||||
QAction *action = actions_[i];
|
||||
THUMBBUTTON *button = &buttons[i];
|
||||
|
||||
button->iId = i;
|
||||
SetupButton(action, button);
|
||||
if (buttons->hIcon) DestroyIcon(buttons->hIcon);
|
||||
|
||||
}
|
||||
|
||||
taskbar_list->ThumbBarUpdateButtons((HWND)widget_->winId(), actions_.count(), buttons);
|
||||
HRESULT hr = taskbar_list->ThumbBarUpdateButtons(reinterpret_cast<HWND>(widget_->winId()), actions_.count(), buttons);
|
||||
if (hr != S_OK) {
|
||||
qLog(Debug) << "Failed to update buttons" << Qt::hex << DWORD (hr);
|
||||
}
|
||||
|
||||
for (int i = 0 ; i < actions_.count() ; ++i) {
|
||||
if (buttons[i].hIcon) {
|
||||
DestroyIcon(buttons[i].hIcon);
|
||||
}
|
||||
}
|
||||
|
||||
taskbar_list->Release();
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2020, 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,24 +22,22 @@
|
||||
#ifndef WINDOWS7THUMBBAR_H
|
||||
#define WINDOWS7THUMBBAR_H
|
||||
|
||||
#include "config.h"
|
||||
#include <windows.h>
|
||||
#include <shobjidl.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QAction>
|
||||
|
||||
#ifndef Q_OS_WIN32
|
||||
typedef void MSG;
|
||||
#endif // Q_OS_WIN32
|
||||
class QTimer;
|
||||
class QAction;
|
||||
|
||||
class Windows7ThumbBar : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// Creates a list of buttons in the taskbar icon for this window. Does nothing and is safe to use on other operating systems too.
|
||||
explicit Windows7ThumbBar(QWidget *widget = 0);
|
||||
public:
|
||||
// Creates a list of buttons in the taskbar icon for this window.
|
||||
explicit Windows7ThumbBar(QWidget *widget = nullptr);
|
||||
|
||||
static const int kIconSize;
|
||||
static const int kMaxButtonCount;
|
||||
@@ -49,17 +48,20 @@ public:
|
||||
// Call this from the parent's winEvent() function.
|
||||
void HandleWinEvent(MSG *msg);
|
||||
|
||||
private slots:
|
||||
private:
|
||||
ITaskbarList3 *CreateTaskbarList();
|
||||
void SetupButton(const QAction *action, THUMBBUTTON *button);
|
||||
|
||||
private slots:
|
||||
void ActionChangedTriggered();
|
||||
void ActionChanged();
|
||||
|
||||
private:
|
||||
private:
|
||||
QWidget *widget_;
|
||||
QTimer *timer_;
|
||||
QList<QAction*> actions_;
|
||||
|
||||
unsigned int button_created_message_id_;
|
||||
|
||||
// Really an ITaskbarList3* but I don't want to have to include windows.h here
|
||||
void *taskbar_list_;
|
||||
};
|
||||
|
||||
#endif // WINDOWS7THUMBBAR_H
|
||||
|
||||
@@ -223,7 +223,7 @@ void DeezerCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
|
||||
QMap<QUrl, CoverSearchResult> results;
|
||||
int i = 0;
|
||||
for (const QJsonValue &json_value : array_data) {
|
||||
for (const QJsonValue json_value : array_data) {
|
||||
|
||||
if (!json_value.isObject()) {
|
||||
Error("Invalid Json reply, data array value is not a object.", json_value);
|
||||
|
||||
@@ -273,7 +273,7 @@ void DiscogsCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id)
|
||||
array_results = value_results.toArray();
|
||||
}
|
||||
|
||||
for (const QJsonValue &value_result : array_results) {
|
||||
for (const QJsonValue value_result : array_results) {
|
||||
|
||||
if (!value_result.isObject()) {
|
||||
Error("Invalid Json reply, results value is not a object.", value_result);
|
||||
@@ -380,7 +380,7 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
|
||||
QJsonArray array_artists = value_artists.toArray();
|
||||
int i = 0;
|
||||
QString artist;
|
||||
for (const QJsonValue &value_artist : array_artists) {
|
||||
for (const QJsonValue value_artist : array_artists) {
|
||||
if (!value_artist.isObject()) {
|
||||
Error("Invalid Json reply, atists array value is not a object.", value_artist);
|
||||
continue;
|
||||
@@ -421,7 +421,7 @@ void DiscogsCoverProvider::HandleReleaseReply(QNetworkReply *reply, const int se
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QJsonValue &value_image : array_images) {
|
||||
for (const QJsonValue value_image : array_images) {
|
||||
|
||||
if (!value_image.isObject()) {
|
||||
Error("Invalid Json reply, images array value is not an object.", value_image);
|
||||
|
||||
@@ -230,7 +230,7 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons
|
||||
}
|
||||
QJsonArray array_type = value_type.toArray();
|
||||
|
||||
for (const QJsonValue &value : array_type) {
|
||||
for (const QJsonValue value : array_type) {
|
||||
|
||||
if (!value.isObject()) {
|
||||
Error("Invalid Json reply, value in albummatches/trackmatches array is not a object.", value);
|
||||
@@ -255,7 +255,7 @@ void LastFmCoverProvider::QueryFinished(QNetworkReply *reply, const int id, cons
|
||||
QJsonArray array_image = json_image.toArray();
|
||||
QUrl url;
|
||||
LastFmImageSize size(LastFmImageSize::Unknown);
|
||||
for (const QJsonValue &value_image : array_image) {
|
||||
for (const QJsonValue value_image : array_image) {
|
||||
if (!value_image.isObject()) {
|
||||
Error("Invalid Json reply, album image value is not an object.", value_image);
|
||||
continue;
|
||||
|
||||
@@ -167,7 +167,7 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QJsonValue &value_release : array_releases) {
|
||||
for (const QJsonValue value_release : array_releases) {
|
||||
|
||||
if (!value_release.isObject()) {
|
||||
Error("Invalid Json reply, releases array value is not an object.", value_release);
|
||||
@@ -187,7 +187,7 @@ void MusicbrainzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
QJsonArray array_artists = json_artists.toArray();
|
||||
int i = 0;
|
||||
QString artist;
|
||||
for (const QJsonValue &value_artist : array_artists) {
|
||||
for (const QJsonValue value_artist : array_artists) {
|
||||
if (!value_artist.isObject()) {
|
||||
Error("Invalid Json reply, artist is not a object.", value_artist);
|
||||
continue;
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextCodec>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
@@ -122,14 +121,7 @@ void MusixmatchCoverProvider::HandleSearchReply(QNetworkReply *reply, const int
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCodec *codec = QTextCodec::codecForName("utf-8");
|
||||
if (!codec) {
|
||||
emit SearchFinished(id, results);
|
||||
return;
|
||||
}
|
||||
QString content = codec->toUnicode(data);
|
||||
|
||||
QString content = data;
|
||||
QString data_begin = "var __mxmState = ";
|
||||
QString data_end = ";</script>";
|
||||
int begin_idx = content.indexOf(data_begin);
|
||||
|
||||
@@ -218,7 +218,7 @@ void QobuzCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
}
|
||||
QJsonArray array_items = value_items.toArray();
|
||||
|
||||
for (const QJsonValue &value : array_items) {
|
||||
for (const QJsonValue value : array_items) {
|
||||
|
||||
if (!value.isObject()) {
|
||||
Error("Invalid Json reply, value in items is not a object.", value);
|
||||
|
||||
@@ -496,7 +496,7 @@ void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id,
|
||||
}
|
||||
|
||||
CoverSearchResults results;
|
||||
for (const QJsonValue &value_item : array_items) {
|
||||
for (const QJsonValue value_item : array_items) {
|
||||
|
||||
if (!value_item.isObject()) {
|
||||
continue;
|
||||
@@ -516,14 +516,14 @@ void SpotifyCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id,
|
||||
QString album = obj_album["name"].toString();
|
||||
|
||||
QStringList artists;
|
||||
for (const QJsonValue &value_artist : array_artists) {
|
||||
for (const QJsonValue value_artist : array_artists) {
|
||||
if (!value_artist.isObject()) continue;
|
||||
QJsonObject obj_artist = value_artist.toObject();
|
||||
if (!obj_artist.contains("name")) continue;
|
||||
artists << obj_artist["name"].toString();
|
||||
}
|
||||
|
||||
for (const QJsonValue &value_image : array_images) {
|
||||
for (const QJsonValue value_image : array_images) {
|
||||
if (!value_image.isObject()) continue;
|
||||
QJsonObject obj_image = value_image.toObject();
|
||||
if (!obj_image.contains("url") || !obj_image.contains("width") || !obj_image.contains("height")) continue;
|
||||
|
||||
@@ -212,7 +212,7 @@ void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) {
|
||||
|
||||
CoverSearchResults results;
|
||||
int i = 0;
|
||||
for (const QJsonValue &value_item : array_items) {
|
||||
for (const QJsonValue value_item : array_items) {
|
||||
|
||||
if (!value_item.isObject()) {
|
||||
Error("Invalid Json reply, items array item is not a object.", value_item);
|
||||
|
||||
31
src/dbus/org.kde.KGlobalAccel.Component.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<node>
|
||||
<interface name="org.kde.kglobalaccel.Component">
|
||||
<property name="friendlyName" type="s" access="read"/>
|
||||
<property name="uniqueName" type="s" access="read"/>
|
||||
<signal name="globalShortcutPressed">
|
||||
<arg name="componentUnique" type="s" direction="out"/>
|
||||
<arg name="actionUnique" type="s" direction="out"/>
|
||||
<arg name="timestamp" type="x" direction="out"/>
|
||||
</signal>
|
||||
<method name="cleanUp">
|
||||
<arg type="b" direction="out"/>
|
||||
</method>
|
||||
<method name="isActive">
|
||||
<arg type="b" direction="out"/>
|
||||
</method>
|
||||
<method name="shortcutNames">
|
||||
<arg type="as" direction="out"/>
|
||||
<arg name="context" type="s" direction="in"/>
|
||||
</method>
|
||||
<method name="shortcutNames">
|
||||
<arg type="as" direction="out"/>
|
||||
</method>
|
||||
<method name="getShortcutContexts">
|
||||
<arg type="as" direction="out"/>
|
||||
</method>
|
||||
<method name="invokeShortcut">
|
||||
<arg name="actionName" type="s" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
||||
71
src/dbus/org.kde.KGlobalAccel.xml
Normal file
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<node>
|
||||
<interface name="org.kde.KGlobalAccel">
|
||||
<signal name="yourShortcutGotChanged">
|
||||
<arg name="actionId" type="as" direction="out"/>
|
||||
<arg name="newKeys" type="ai" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QList<int>"/>
|
||||
</signal>
|
||||
<method name="allComponents">
|
||||
<arg type="ao" direction="out"/>
|
||||
</method>
|
||||
<method name="allMainComponents">
|
||||
<arg type="aas" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList<QStringList>"/>
|
||||
</method>
|
||||
<method name="allActionsForComponent">
|
||||
<arg type="aas" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList<QStringList>"/>
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="action">
|
||||
<arg type="as" direction="out"/>
|
||||
<arg name="key" type="i" direction="in"/>
|
||||
</method>
|
||||
<method name="getComponent">
|
||||
<arg type="o" direction="out"/>
|
||||
<arg name="componentUnique" type="s" direction="in"/>
|
||||
</method>
|
||||
<method name="shortcut">
|
||||
<arg type="ai" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList<int>"/>
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="defaultShortcut">
|
||||
<arg type="ai" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList<int>"/>
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="setShortcut">
|
||||
<arg type="ai" direction="out"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList<int>"/>
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
<arg name="keys" type="ai" direction="in"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QList<int>"/>
|
||||
<arg name="flags" type="u" direction="in"/>
|
||||
</method>
|
||||
<method name="setForeignShortcut">
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
<arg name="keys" type="ai" direction="in"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QList<int>"/>
|
||||
</method>
|
||||
<method name="setInactive">
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="doRegister">
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="unRegister">
|
||||
<arg name="actionId" type="as" direction="in"/>
|
||||
</method>
|
||||
<method name="activateGlobalShortcutContext">
|
||||
<arg name="component" type="s" direction="in"/>
|
||||
<arg name="context" type="s" direction="in"/>
|
||||
</method>
|
||||
<method name="isGlobalShortcutAvailable">
|
||||
<arg type="b" direction="out"/>
|
||||
<arg name="key" type="i" direction="in"/>
|
||||
<arg name="component" type="s" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
||||
@@ -199,7 +199,7 @@ void CddaSongLoader::LoadSongs() {
|
||||
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
|
||||
g_free(string_mb);
|
||||
gst_message_unref(msg_tag);
|
||||
gst_tag_list_free(tags);
|
||||
gst_tag_list_unref(tags);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -40,7 +40,7 @@ class DeviceLister : public QObject {
|
||||
DeviceLister();
|
||||
~DeviceLister() override;
|
||||
|
||||
// Tries to start the thread and initialise the engine. This object will be moved to the new thread.
|
||||
// Tries to start the thread and initialize the engine. This object will be moved to the new thread.
|
||||
void Start();
|
||||
virtual void ExitAsync();
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <QObject>
|
||||
#include <QMetaObject>
|
||||
#include <QThread>
|
||||
#include <QtConcurrent>
|
||||
#include <QAbstractItemModel>
|
||||
#include <QDir>
|
||||
#include <QList>
|
||||
@@ -46,7 +47,6 @@
|
||||
#include "devicemanager.h"
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/concurrentrun.h"
|
||||
#include "core/database.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/logging.h"
|
||||
@@ -102,7 +102,11 @@ DeviceManager::DeviceManager(Application *app, QObject *parent)
|
||||
connect(this, SIGNAL(DeviceCreatedFromDB(DeviceInfo*)), SLOT(AddDeviceFromDB(DeviceInfo*)));
|
||||
|
||||
// This reads from the database and contents on the database mutex, which can be very slow on startup.
|
||||
ConcurrentRun::Run<void>(&thread_pool_, std::bind(&DeviceManager::LoadAllDevices, this));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
(void)QtConcurrent::run(&thread_pool_, &DeviceManager::LoadAllDevices, this);
|
||||
#else
|
||||
(void)QtConcurrent::run(&thread_pool_, this, &DeviceManager::LoadAllDevices);
|
||||
#endif
|
||||
|
||||
// This proxy model only shows connected devices
|
||||
connected_devices_model_ = new DeviceStateFilterModel(this);
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
class DeviceLister;
|
||||
class DeviceManager;
|
||||
|
||||
bool MtpDevice::sInitialisedLibMTP = false;
|
||||
bool MtpDevice::sInitializedLibMTP = false;
|
||||
|
||||
MtpDevice::MtpDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time)
|
||||
: ConnectedDevice(url, lister, unique_id, manager, app, database_id, first_time),
|
||||
@@ -55,9 +55,9 @@ MtpDevice::MtpDevice(const QUrl &url, DeviceLister *lister, const QString &uniqu
|
||||
loader_thread_(nullptr),
|
||||
closing_(false) {
|
||||
|
||||
if (!sInitialisedLibMTP) {
|
||||
if (!sInitializedLibMTP) {
|
||||
LIBMTP_Init();
|
||||
sInitialisedLibMTP = true;
|
||||
sInitializedLibMTP = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ class MtpDevice : public ConnectedDevice {
|
||||
int GetCapacity(LIBMTP_mtpdevice_struct* device);
|
||||
|
||||
private:
|
||||
static bool sInitialisedLibMTP;
|
||||
static bool sInitializedLibMTP;
|
||||
|
||||
MtpLoader *loader_;
|
||||
QThread *loader_thread_;
|
||||
|
||||
@@ -10,18 +10,6 @@
|
||||
<height>500</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::StrongFocus</enum>
|
||||
</property>
|
||||
|
||||
@@ -293,7 +293,11 @@ void EditTagDialog::SetSongs(const SongList &s, const PlaylistItemList &items) {
|
||||
ui_->song_list->clear();
|
||||
|
||||
// Reload tags in the background
|
||||
QFuture<QList<Data>> future = QtConcurrent::run(std::bind(&EditTagDialog::LoadData, this, s));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QFuture<QList<Data>> future = QtConcurrent::run(&EditTagDialog::LoadData, this, s);
|
||||
#else
|
||||
QFuture<QList<Data>> future = QtConcurrent::run(this, &EditTagDialog::LoadData, s);
|
||||
#endif
|
||||
NewClosure(future, this, SLOT(SetSongsFinished(QFuture<QList<EditTagDialog::Data>>)), future);
|
||||
|
||||
}
|
||||
@@ -721,13 +725,16 @@ void EditTagDialog::UpdateCoverOf(const Song &selected, const QModelIndexList &s
|
||||
|
||||
// Now check if we have any other songs cached that share that artist and album (and would therefore be changed as well)
|
||||
for (int i = 0; i < data_.count(); ++i) {
|
||||
if (i == sel.first().row()) // Already changed this one
|
||||
continue;
|
||||
|
||||
Song *other_song = &data_[i].original_;
|
||||
if (selected.artist() == other_song->artist() && selected.album() == other_song->album()) {
|
||||
other_song->set_art_manual(cover_url);
|
||||
if (i != sel.first().row()) {
|
||||
Song *other_song = &data_[i].original_;
|
||||
if (selected.effective_albumartist() == other_song->effective_albumartist() && selected.album() == other_song->album()) {
|
||||
other_song->set_art_manual(cover_url);
|
||||
}
|
||||
}
|
||||
|
||||
data_[i].current_.set_art_manual(data_[i].original_.art_manual());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
105
src/dialogs/snapdialog.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QString>
|
||||
#include <QFont>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QKeySequence>
|
||||
#include <QSettings>
|
||||
|
||||
#include "snapdialog.h"
|
||||
#include "ui_snapdialog.h"
|
||||
|
||||
#include "core/mainwindow.h"
|
||||
|
||||
SnapDialog::SnapDialog(QWidget *parent) : QDialog(parent), ui_(new Ui_SnapDialog) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
setWindowFlags(this->windowFlags() | Qt::WindowStaysOnTopHint);
|
||||
setWindowTitle(tr("Strawberry is running as a Snap"));
|
||||
|
||||
QString text;
|
||||
text += QString("<p>");
|
||||
text += tr("It is detected that Strawberry is running as a Snap");
|
||||
text += QString("</p>");
|
||||
|
||||
text += QString("<p>");
|
||||
text += tr("Strawberry is slower, and has restrictions when running as a Snap. Accessing the root filesystem (/) will not work. There also might be other restrictions such as accessing certain devices or network shares.");
|
||||
text += QString("</p>");
|
||||
|
||||
text += QString("<p>");
|
||||
text += QString("Strawberry is available natively in the official package repositories for Fedora, openSUSE, Mageia, Arch, Manjaro, MX Linux and most other popular Linux distributions.");
|
||||
text += QString("</p>");
|
||||
|
||||
text += QString("<p>");
|
||||
text += tr("For Ubuntu there is an official PPA repository available at %1.").arg(QString("<a style=\"color:%1;\" href=\"https://launchpad.net/~jonaski/+archive/ubuntu/strawberry\">https://launchpad.net/~jonaski/+archive/ubuntu/strawberry</a>").arg(palette().text().color().name()));
|
||||
text += QString("</p>");
|
||||
|
||||
text += QString("<p>");
|
||||
text += tr("Official releases are available for Debian and Ubuntu which also work on most of their derivatives. See %1 for more information.").arg(QString("<a style=\"color:%1;\" href=\"https://www.strawberrymusicplayer.org/\">https://www.strawberrymusicplayer.org/</a>").arg(palette().text().color().name()));
|
||||
text += QString("</p>");
|
||||
|
||||
text += QString("<p>");
|
||||
text += tr("For a better experience please consider the other options above.");
|
||||
text += QString("</p>");
|
||||
|
||||
text += QString("<p>");
|
||||
text += tr("Copy your strawberry.conf and strawberry.db from your ~/snap directory to avoid losing configration before you uninstall the snap:");
|
||||
text += QString("<br />");
|
||||
text += QString("cp ~/snap/strawberry/current/.config/strawberry/strawberry.conf ~/.config/strawberry/strawberry.conf<br />");
|
||||
text += QString("cp ~/snap/strawberry/current/.local/share/strawberry/strawberry/strawberry.db ~/.local/share/strawberry/strawberry/strawberry.db<br />");
|
||||
text += QString("</p>");
|
||||
text += QString("<p>");
|
||||
text += tr("Uninstall the snap with:");
|
||||
text += QString("<br />");
|
||||
text += QString("snap remove strawberry");
|
||||
text += QString("</p>");
|
||||
text += QString("<p>");
|
||||
text += tr("Install strawberry through PPA:<br />");
|
||||
text += QString("sudo add-apt-repository ppa:jonaski/strawberry<br />");
|
||||
text += QString("sudo apt-get update<br />");
|
||||
text += QString("sudo apt install strawberry");
|
||||
text += QString("</p>");
|
||||
text += QString("<p></p>");
|
||||
|
||||
ui_->label_text->setText(text);
|
||||
ui_->label_text->adjustSize();
|
||||
adjustSize();
|
||||
|
||||
ui_->buttonBox->button(QDialogButtonBox::Ok)->setShortcut(QKeySequence::Close);
|
||||
|
||||
connect(ui_->checkbox_do_not_show_message_again, SIGNAL(toggled(bool)), SLOT(DoNotShowMessageAgain()));
|
||||
|
||||
}
|
||||
|
||||
SnapDialog::~SnapDialog() { delete ui_; }
|
||||
|
||||
void SnapDialog::DoNotShowMessageAgain() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(MainWindow::kSettingsGroup);
|
||||
s.setValue("ignore_snap", ui_->checkbox_do_not_show_message_again->isChecked());
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
41
src/dialogs/snapdialog.h
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef SNAPDIALOG_H
|
||||
#define SNAPDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class Ui_SnapDialog;
|
||||
|
||||
class SnapDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SnapDialog(QWidget *parent = nullptr);
|
||||
~SnapDialog() override;
|
||||
|
||||
private slots:
|
||||
void DoNotShowMessageAgain();
|
||||
|
||||
private:
|
||||
Ui_SnapDialog *ui_;
|
||||
};
|
||||
|
||||
#endif // SNAPDIALOG_H
|
||||
154
src/dialogs/snapdialog.ui
Normal file
@@ -0,0 +1,154 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SnapDialog</class>
|
||||
<widget class="QDialog" name="SnapDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::StrongFocus</enum>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Strawberry is running as a snap</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../data/icons.qrc">
|
||||
<normaloff>:/icons/64x64/strawberry.png</normaloff>:/icons/64x64/strawberry.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="layout_left">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_logo">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="../../data/icons.qrc">:/icons/64x64/dialog-warning.png</pixmap>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="spacer_left">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="layout_right">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_text">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>1</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkbox_do_not_show_message_again">
|
||||
<property name="text">
|
||||
<string>Do not show this message again.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
<include location="../../data/icons.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>SnapDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>SnapDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -35,8 +35,7 @@
|
||||
#include "alsadevicefinder.h"
|
||||
|
||||
AlsaDeviceFinder::AlsaDeviceFinder()
|
||||
: DeviceFinder("alsa", {"alsa","alsasink"}) {
|
||||
}
|
||||
: DeviceFinder("alsa", {"alsa","alsasink"}) {}
|
||||
|
||||
QList<DeviceFinder::Device> AlsaDeviceFinder::ListDevices() {
|
||||
|
||||
@@ -45,7 +44,7 @@ QList<DeviceFinder::Device> AlsaDeviceFinder::ListDevices() {
|
||||
snd_pcm_stream_name(SND_PCM_STREAM_PLAYBACK);
|
||||
|
||||
int card = -1;
|
||||
snd_ctl_card_info_t* cardinfo;
|
||||
snd_ctl_card_info_t *cardinfo = nullptr;
|
||||
snd_ctl_card_info_alloca(&cardinfo);
|
||||
while (true) {
|
||||
|
||||
@@ -59,7 +58,7 @@ QList<DeviceFinder::Device> AlsaDeviceFinder::ListDevices() {
|
||||
char str[32];
|
||||
snprintf(str, sizeof(str) - 1, "hw:%d", card);
|
||||
|
||||
snd_ctl_t* handle;
|
||||
snd_ctl_t *handle = nullptr;
|
||||
result = snd_ctl_open(&handle, str, 0);
|
||||
if (result < 0) {
|
||||
qLog(Error) << "Unable to open soundcard" << card << ":" << snd_strerror(result);
|
||||
@@ -75,7 +74,7 @@ QList<DeviceFinder::Device> AlsaDeviceFinder::ListDevices() {
|
||||
}
|
||||
|
||||
int dev = -1;
|
||||
snd_pcm_info_t* pcminfo;
|
||||
snd_pcm_info_t *pcminfo = nullptr;
|
||||
snd_pcm_info_alloca(&pcminfo);
|
||||
while (true) {
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class AlsaDeviceFinder : public DeviceFinder {
|
||||
public:
|
||||
explicit AlsaDeviceFinder();
|
||||
|
||||
bool Initialise() override { return true; }
|
||||
bool Initialize() override { return true; }
|
||||
QList<Device> ListDevices() override;
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class DeviceFinder {
|
||||
void add_output(const QString output) { outputs_.append(output); }
|
||||
|
||||
// Does any necessary setup, returning false if this DeviceFinder cannot be used.
|
||||
virtual bool Initialise() = 0;
|
||||
virtual bool Initialize() = 0;
|
||||
|
||||
// Returns a list of available devices.
|
||||
virtual QList<Device> ListDevices() = 0;
|
||||
|
||||
@@ -70,8 +70,8 @@ void DeviceFinders::Init() {
|
||||
#endif
|
||||
|
||||
for (DeviceFinder *finder : device_finders) {
|
||||
if (!finder->Initialise()) {
|
||||
qLog(Warning) << "Failed to initialise DeviceFinder for" << finder->name();
|
||||
if (!finder->Initialize()) {
|
||||
qLog(Warning) << "Failed to initialize DeviceFinder for" << finder->name();
|
||||
delete finder;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class DirectSoundDeviceFinder : public DeviceFinder {
|
||||
public:
|
||||
explicit DirectSoundDeviceFinder();
|
||||
|
||||
virtual bool Initialise() { return true; }
|
||||
virtual bool Initialize() { return true; }
|
||||
virtual QList<Device> ListDevices();
|
||||
|
||||
private:
|
||||
|
||||
@@ -31,9 +31,11 @@
|
||||
#include <QSettings>
|
||||
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/networkproxyfactory.h"
|
||||
#include "engine_fwd.h"
|
||||
#include "enginebase.h"
|
||||
#include "settings/backendsettingspage.h"
|
||||
#include "settings/networkproxysettingspage.h"
|
||||
|
||||
Engine::Base::Base()
|
||||
: volume_(100),
|
||||
@@ -56,6 +58,7 @@ Engine::Base::Base()
|
||||
fadeout_pause_enabled_(false),
|
||||
fadeout_duration_(2),
|
||||
fadeout_duration_nanosec_(2 * kNsecPerSec),
|
||||
proxy_authentication_(false),
|
||||
about_to_end_emitted_(false) {}
|
||||
|
||||
Engine::Base::~Base() {}
|
||||
@@ -127,6 +130,32 @@ void Engine::Base::ReloadSettings() {
|
||||
|
||||
s.endGroup();
|
||||
|
||||
s.beginGroup(NetworkProxySettingsPage::kSettingsGroup);
|
||||
NetworkProxyFactory::Mode proxy_mode = NetworkProxyFactory::Mode(s.value("mode", NetworkProxyFactory::Mode_System).toInt());
|
||||
if (proxy_mode == NetworkProxyFactory::Mode_Manual && s.contains("engine") && s.value("engine").toBool()) {
|
||||
QString proxy_host = s.value("hostname").toString();
|
||||
int proxy_port = s.value("port").toInt();
|
||||
if (proxy_host.isEmpty() || proxy_port <= 0) {
|
||||
proxy_address_.clear();
|
||||
proxy_authentication_ = false;
|
||||
proxy_user_.clear();
|
||||
proxy_pass_.clear();
|
||||
}
|
||||
else {
|
||||
proxy_address_ = QString("%1:%2").arg(proxy_host).arg(proxy_port);
|
||||
proxy_authentication_ = s.value("use_authentication").toBool();
|
||||
proxy_user_ = s.value("username").toString();
|
||||
proxy_pass_ = s.value("password").toString();
|
||||
}
|
||||
}
|
||||
else {
|
||||
proxy_address_.clear();
|
||||
proxy_authentication_ = false;
|
||||
proxy_user_.clear();
|
||||
proxy_pass_.clear();
|
||||
}
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void Engine::Base::EmitAboutToEnd() {
|
||||
|
||||
@@ -200,6 +200,12 @@ public:
|
||||
qint64 fadeout_pause_duration_;
|
||||
qint64 fadeout_pause_duration_nanosec_;
|
||||
|
||||
// Proxy
|
||||
QString proxy_address_;
|
||||
bool proxy_authentication_;
|
||||
QString proxy_user_;
|
||||
QString proxy_pass_;
|
||||
|
||||
private:
|
||||
bool about_to_end_emitted_;
|
||||
Q_DISABLE_COPY(Base)
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <string>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/pbutils/pbutils.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QFuture>
|
||||
@@ -53,6 +54,7 @@
|
||||
#include "core/logging.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/signalchecker.h"
|
||||
#include "enginebase.h"
|
||||
#include "enginetype.h"
|
||||
#include "gstengine.h"
|
||||
@@ -71,9 +73,12 @@ const char *GstEngine::kAVDTPSink = "avdtpsink";
|
||||
const char *GstEngine::InterAudiosink = "interaudiosink";
|
||||
const char *GstEngine::kDirectSoundSink = "directsoundsink";
|
||||
const char *GstEngine::kOSXAudioSink = "osxaudiosink";
|
||||
const int GstEngine::kDiscoveryTimeoutS = 10;
|
||||
|
||||
GstEngine::GstEngine(TaskManager *task_manager)
|
||||
: task_manager_(task_manager),
|
||||
gst_startup_(nullptr),
|
||||
discoverer_(nullptr),
|
||||
buffering_task_id_(-1),
|
||||
latest_buffer_(nullptr),
|
||||
stereo_balancer_enabled_(false),
|
||||
@@ -86,7 +91,10 @@ GstEngine::GstEngine(TaskManager *task_manager)
|
||||
is_fading_out_to_pause_(false),
|
||||
has_faded_out_(false),
|
||||
scope_chunk_(0),
|
||||
have_new_buffer_(false) {
|
||||
have_new_buffer_(false),
|
||||
discovery_finished_cb_id_(-1),
|
||||
discovery_discovered_cb_id_(-1)
|
||||
{
|
||||
|
||||
type_ = Engine::GStreamer;
|
||||
seek_timer_->setSingleShot(true);
|
||||
@@ -99,7 +107,7 @@ GstEngine::GstEngine(TaskManager *task_manager)
|
||||
|
||||
GstEngine::~GstEngine() {
|
||||
|
||||
EnsureInitialised();
|
||||
EnsureInitialized();
|
||||
current_pipeline_.reset();
|
||||
|
||||
if (latest_buffer_) {
|
||||
@@ -107,6 +115,19 @@ GstEngine::~GstEngine() {
|
||||
latest_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
if (discoverer_) {
|
||||
|
||||
if (discovery_discovered_cb_id_ != -1)
|
||||
g_signal_handler_disconnect(G_OBJECT(discoverer_), discovery_discovered_cb_id_);
|
||||
if (discovery_finished_cb_id_ != -1)
|
||||
g_signal_handler_disconnect(G_OBJECT(discoverer_), discovery_finished_cb_id_);
|
||||
|
||||
gst_discoverer_stop(discoverer_);
|
||||
g_object_unref(discoverer_);
|
||||
discoverer_ = nullptr;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool GstEngine::Init() {
|
||||
@@ -136,19 +157,28 @@ Engine::State GstEngine::state() const {
|
||||
|
||||
void GstEngine::StartPreloading(const QUrl &stream_url, const QUrl &original_url, const bool force_stop_at_end, const qint64 beginning_nanosec, const qint64 end_nanosec) {
|
||||
|
||||
EnsureInitialised();
|
||||
EnsureInitialized();
|
||||
|
||||
QByteArray gst_url = FixupUrl(stream_url);
|
||||
|
||||
// No crossfading, so we can just queue the new URL in the existing pipeline and get gapless playback (hopefully)
|
||||
if (current_pipeline_)
|
||||
if (current_pipeline_) {
|
||||
current_pipeline_->SetNextUrl(gst_url, original_url, beginning_nanosec, force_stop_at_end ? end_nanosec : 0);
|
||||
// Add request to discover the stream
|
||||
#ifdef Q_OS_LINUX
|
||||
if (discoverer_) {
|
||||
if (!gst_discoverer_discover_uri_async(discoverer_, gst_url.toStdString().c_str())) {
|
||||
qLog(Error) << "Failed to start stream discovery for" << gst_url;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool GstEngine::Load(const QUrl &stream_url, const QUrl &original_url, Engine::TrackChangeFlags change, const bool force_stop_at_end, const quint64 beginning_nanosec, const qint64 end_nanosec) {
|
||||
|
||||
EnsureInitialised();
|
||||
EnsureInitialized();
|
||||
|
||||
Engine::Base::Load(stream_url, original_url, change, force_stop_at_end, beginning_nanosec, end_nanosec);
|
||||
|
||||
@@ -180,13 +210,34 @@ bool GstEngine::Load(const QUrl &stream_url, const QUrl &original_url, Engine::T
|
||||
if (crossfade)
|
||||
current_pipeline_->StartFader(fadeout_duration_nanosec_, QTimeLine::Forward);
|
||||
|
||||
// Setting up stream discoverer
|
||||
#ifdef Q_OS_LINUX
|
||||
if (!discoverer_) {
|
||||
discoverer_ = gst_discoverer_new(kDiscoveryTimeoutS * GST_SECOND, nullptr);
|
||||
if (discoverer_) {
|
||||
discovery_discovered_cb_id_ = CHECKED_GCONNECT(G_OBJECT(discoverer_), "discovered", &StreamDiscovered, this);
|
||||
discovery_finished_cb_id_ = CHECKED_GCONNECT(G_OBJECT(discoverer_), "finished", &StreamDiscoveryFinished, this);
|
||||
gst_discoverer_start(discoverer_);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Add request to discover the stream
|
||||
#ifdef Q_OS_LINUX
|
||||
if (discoverer_) {
|
||||
if (!gst_discoverer_discover_uri_async(discoverer_, gst_url.toStdString().c_str())) {
|
||||
qLog(Error) << "Failed to start stream discovery for" << gst_url;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool GstEngine::Play(const quint64 offset_nanosec) {
|
||||
|
||||
EnsureInitialised();
|
||||
EnsureInitialized();
|
||||
|
||||
if (!current_pipeline_ || current_pipeline_->is_buffering()) return false;
|
||||
|
||||
@@ -344,7 +395,7 @@ const Engine::Scope &GstEngine::scope(const int chunk_length) {
|
||||
|
||||
EngineBase::OutputDetailsList GstEngine::GetOutputsList() const {
|
||||
|
||||
const_cast<GstEngine*>(this)->EnsureInitialised();
|
||||
const_cast<GstEngine*>(this)->EnsureInitialized();
|
||||
|
||||
EngineBase::OutputDetailsList ret;
|
||||
|
||||
@@ -369,7 +420,7 @@ EngineBase::OutputDetailsList GstEngine::GetOutputsList() const {
|
||||
|
||||
bool GstEngine::ValidOutput(const QString &output) {
|
||||
|
||||
EnsureInitialised();
|
||||
EnsureInitialized();
|
||||
|
||||
PluginDetailsList plugins = GetPluginList("Sink/Audio");
|
||||
for (const PluginDetails &plugin : plugins) {
|
||||
@@ -645,7 +696,7 @@ void GstEngine::BufferingFinished() {
|
||||
|
||||
GstEngine::PluginDetailsList GstEngine::GetPluginList(const QString &classname) const {
|
||||
|
||||
const_cast<GstEngine*>(this)->EnsureInitialised();
|
||||
const_cast<GstEngine*>(this)->EnsureInitialized();
|
||||
|
||||
PluginDetailsList ret;
|
||||
|
||||
@@ -672,7 +723,7 @@ GstEngine::PluginDetailsList GstEngine::GetPluginList(const QString &classname)
|
||||
|
||||
QByteArray GstEngine::FixupUrl(const QUrl &url) {
|
||||
|
||||
EnsureInitialised();
|
||||
EnsureInitialized();
|
||||
|
||||
QByteArray uri;
|
||||
|
||||
@@ -749,7 +800,7 @@ void GstEngine::StopTimers() {
|
||||
|
||||
std::shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline() {
|
||||
|
||||
EnsureInitialised();
|
||||
EnsureInitialized();
|
||||
|
||||
std::shared_ptr<GstEnginePipeline> ret(new GstEnginePipeline(this));
|
||||
ret->set_output_device(output_, device_);
|
||||
@@ -760,6 +811,7 @@ std::shared_ptr<GstEnginePipeline> GstEngine::CreatePipeline() {
|
||||
ret->set_buffer_duration_nanosec(buffer_duration_nanosec_);
|
||||
ret->set_buffer_low_watermark(buffer_low_watermark_);
|
||||
ret->set_buffer_high_watermark(buffer_high_watermark_);
|
||||
ret->set_proxy_settings(proxy_address_, proxy_authentication_, proxy_user_, proxy_pass_);
|
||||
|
||||
ret->AddBufferConsumer(this);
|
||||
for (GstBufferConsumer *consumer : buffer_consumers_) {
|
||||
@@ -845,3 +897,69 @@ void GstEngine::UpdateScope(const int chunk_length) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GstEngine::StreamDiscovered(GstDiscoverer*, GstDiscovererInfo *info, GError*, gpointer self) {
|
||||
|
||||
GstEngine *instance = reinterpret_cast<GstEngine*>(self);
|
||||
if (!instance->current_pipeline_) return;
|
||||
|
||||
QString discovered_url(gst_discoverer_info_get_uri(info));
|
||||
|
||||
GstDiscovererResult result = gst_discoverer_info_get_result(info);
|
||||
if (result != GST_DISCOVERER_OK) {
|
||||
QString error_message = GSTdiscovererErrorMessage(result);
|
||||
qLog(Error) << QString("Stream discovery for %1 failed: %2").arg(discovered_url).arg(error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
GList *audio_streams = gst_discoverer_info_get_audio_streams(info);
|
||||
if (audio_streams) {
|
||||
|
||||
GstDiscovererStreamInfo *stream_info = reinterpret_cast<GstDiscovererStreamInfo*>(g_list_first(audio_streams)->data);
|
||||
|
||||
Engine::SimpleMetaBundle bundle;
|
||||
if (discovered_url == instance->current_pipeline_->stream_url()) {
|
||||
bundle.url = instance->current_pipeline_->original_url();
|
||||
}
|
||||
else if (discovered_url == instance->current_pipeline_->next_stream_url()) {
|
||||
bundle.url = instance->current_pipeline_->next_original_url();
|
||||
}
|
||||
bundle.stream_url = QUrl(discovered_url);
|
||||
bundle.samplerate = gst_discoverer_audio_info_get_sample_rate(GST_DISCOVERER_AUDIO_INFO(stream_info));
|
||||
bundle.bitdepth = gst_discoverer_audio_info_get_depth(GST_DISCOVERER_AUDIO_INFO(stream_info));
|
||||
bundle.bitrate = gst_discoverer_audio_info_get_bitrate(GST_DISCOVERER_AUDIO_INFO(stream_info)) / 1000;
|
||||
|
||||
GstCaps *caps = gst_discoverer_stream_info_get_caps(stream_info);
|
||||
gchar *codec_description = gst_pb_utils_get_codec_description(caps);
|
||||
QString filetype_description = (codec_description ? QString(codec_description) : QString("Unknown"));
|
||||
g_free(codec_description);
|
||||
|
||||
gst_caps_unref(caps);
|
||||
gst_discoverer_stream_info_list_free(audio_streams);
|
||||
|
||||
bundle.filetype = Song::FiletypeByDescription(filetype_description);
|
||||
qLog(Info) << "Got stream info for" << discovered_url + ":" << filetype_description;
|
||||
|
||||
emit instance->MetaData(bundle);
|
||||
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Could not detect an audio stream in" << discovered_url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GstEngine::StreamDiscoveryFinished(GstDiscoverer*, gpointer) {}
|
||||
|
||||
QString GstEngine::GSTdiscovererErrorMessage(GstDiscovererResult result) {
|
||||
|
||||
switch (result) {
|
||||
case GST_DISCOVERER_URI_INVALID: return "The URI is invalid";
|
||||
case GST_DISCOVERER_TIMEOUT: return "The discovery timed-out";
|
||||
case GST_DISCOVERER_BUSY: return "The discoverer was already discovering a file";
|
||||
case GST_DISCOVERER_MISSING_PLUGINS: return "Some plugins are missing for full discovery";
|
||||
case GST_DISCOVERER_ERROR:
|
||||
default: return "An error happened and the GError is set";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/pbutils/pbutils.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
@@ -85,7 +86,7 @@ class GstEngine : public Engine::Base, public GstBufferConsumer {
|
||||
bool ALSADeviceSupport(const QString &output) override;
|
||||
|
||||
void SetStartup(GstStartup *gst_startup) { gst_startup_ = gst_startup; }
|
||||
void EnsureInitialised() { gst_startup_->EnsureInitialised(); }
|
||||
void EnsureInitialized() { gst_startup_->EnsureInitialized(); }
|
||||
|
||||
GstElement *CreateElement(const QString &factoryName, GstElement *bin = nullptr, const bool showerror = true);
|
||||
void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) override;
|
||||
@@ -126,19 +127,6 @@ class GstEngine : public Engine::Base, public GstBufferConsumer {
|
||||
void BufferingFinished();
|
||||
|
||||
private:
|
||||
static const char *kAutoSink;
|
||||
static const char *kALSASink;
|
||||
static const char *kOpenALSASink;
|
||||
static const char *kOSSSink;
|
||||
static const char *kOSS4Sink;
|
||||
static const char *kJackAudioSink;
|
||||
static const char *kPulseSink;
|
||||
static const char *kA2DPSink;
|
||||
static const char *kAVDTPSink;
|
||||
static const char *InterAudiosink;
|
||||
static const char *kDirectSoundSink;
|
||||
static const char *kOSXAudioSink;
|
||||
|
||||
PluginDetailsList GetPluginList(const QString &classname) const;
|
||||
QByteArray FixupUrl(const QUrl &url);
|
||||
|
||||
@@ -153,13 +141,32 @@ class GstEngine : public Engine::Base, public GstBufferConsumer {
|
||||
|
||||
void UpdateScope(int chunk_length);
|
||||
|
||||
static void StreamDiscovered(GstDiscoverer*, GstDiscovererInfo *info, GError*, gpointer self);
|
||||
static void StreamDiscoveryFinished(GstDiscoverer*, gpointer);
|
||||
static QString GSTdiscovererErrorMessage(GstDiscovererResult result);
|
||||
|
||||
private:
|
||||
static const char *kAutoSink;
|
||||
static const char *kALSASink;
|
||||
static const char *kOpenALSASink;
|
||||
static const char *kOSSSink;
|
||||
static const char *kOSS4Sink;
|
||||
static const char *kJackAudioSink;
|
||||
static const char *kPulseSink;
|
||||
static const char *kA2DPSink;
|
||||
static const char *kAVDTPSink;
|
||||
static const char *InterAudiosink;
|
||||
static const char *kDirectSoundSink;
|
||||
static const char *kOSXAudioSink;
|
||||
static const int kDiscoveryTimeoutS;
|
||||
static const qint64 kTimerIntervalNanosec = 1000 * kNsecPerMsec; // 1s
|
||||
static const qint64 kPreloadGapNanosec = 5000 * kNsecPerMsec; // 5s
|
||||
static const qint64 kSeekDelayNanosec = 100 * kNsecPerMsec; // 100msec
|
||||
|
||||
TaskManager *task_manager_;
|
||||
GstStartup *gst_startup_;
|
||||
GstDiscoverer *discoverer_;
|
||||
|
||||
int buffering_task_id_;
|
||||
|
||||
std::shared_ptr<GstEnginePipeline> current_pipeline_;
|
||||
@@ -197,6 +204,9 @@ class GstEngine : public Engine::Base, public GstBufferConsumer {
|
||||
int scope_chunks_;
|
||||
QString buffer_format_;
|
||||
|
||||
int discovery_finished_cb_id_;
|
||||
int discovery_discovered_cb_id_;
|
||||
|
||||
};
|
||||
|
||||
#endif /* GSTENGINE_H */
|
||||
|
||||
@@ -28,12 +28,13 @@
|
||||
#include <glib-object.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/audio/audio.h>
|
||||
#include <gst/pbutils/pbutils.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QCoreApplication>
|
||||
#include <QtConcurrent>
|
||||
#include <QMutex>
|
||||
#include <QMetaType>
|
||||
#include <QByteArray>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
@@ -45,7 +46,6 @@
|
||||
#include <QUuid>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/concurrentrun.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/signalchecker.h"
|
||||
#include "core/timeconstants.h"
|
||||
@@ -59,7 +59,6 @@
|
||||
|
||||
const int GstEnginePipeline::kGstStateTimeoutNanosecs = 10000000;
|
||||
const int GstEnginePipeline::kFaderFudgeMsec = 2000;
|
||||
const int GstEnginePipeline::kDiscoveryTimeoutS = 10;
|
||||
|
||||
const int GstEnginePipeline::kEqBandCount = 10;
|
||||
const int GstEnginePipeline::kEqBandFrequencies[] = { 60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000 };
|
||||
@@ -85,6 +84,7 @@ GstEnginePipeline::GstEnginePipeline(GstEngine *engine)
|
||||
buffer_low_watermark_(BackendSettingsPage::kDefaultBufferLowWatermark),
|
||||
buffer_high_watermark_(BackendSettingsPage::kDefaultBufferHighWatermark),
|
||||
buffering_(false),
|
||||
proxy_authentication_(false),
|
||||
segment_start_(0),
|
||||
segment_start_received_(false),
|
||||
end_offset_nanosec_(-1),
|
||||
@@ -92,7 +92,7 @@ GstEnginePipeline::GstEnginePipeline(GstEngine *engine)
|
||||
next_end_offset_nanosec_(-1),
|
||||
ignore_next_seek_(false),
|
||||
ignore_tags_(false),
|
||||
pipeline_is_initialised_(false),
|
||||
pipeline_is_initialized_(false),
|
||||
pipeline_is_connected_(false),
|
||||
pending_seek_nanosec_(-1),
|
||||
last_known_position_ns_(0),
|
||||
@@ -107,13 +107,10 @@ GstEnginePipeline::GstEnginePipeline(GstEngine *engine)
|
||||
audiopanorama_(nullptr),
|
||||
equalizer_(nullptr),
|
||||
equalizer_preamp_(nullptr),
|
||||
discoverer_(nullptr),
|
||||
pad_added_cb_id_(-1),
|
||||
notify_source_cb_id_(-1),
|
||||
about_to_finish_cb_id_(-1),
|
||||
bus_cb_id_(-1),
|
||||
discovery_finished_cb_id_(-1),
|
||||
discovery_discovered_cb_id_(-1),
|
||||
unsupported_analyzer_(false)
|
||||
{
|
||||
|
||||
@@ -127,18 +124,6 @@ GstEnginePipeline::GstEnginePipeline(GstEngine *engine)
|
||||
|
||||
GstEnginePipeline::~GstEnginePipeline() {
|
||||
|
||||
if (discoverer_) {
|
||||
|
||||
if (discovery_discovered_cb_id_ != -1)
|
||||
g_signal_handler_disconnect(G_OBJECT(discoverer_), discovery_discovered_cb_id_);
|
||||
if (discovery_finished_cb_id_ != -1)
|
||||
g_signal_handler_disconnect(G_OBJECT(discoverer_), discovery_finished_cb_id_);
|
||||
|
||||
g_object_unref(discoverer_);
|
||||
discoverer_ = nullptr;
|
||||
|
||||
}
|
||||
|
||||
if (pipeline_) {
|
||||
|
||||
if (pad_added_cb_id_ != -1)
|
||||
@@ -159,8 +144,7 @@ GstEnginePipeline::~GstEnginePipeline() {
|
||||
gst_object_unref(bus);
|
||||
}
|
||||
|
||||
if (state() != GST_STATE_NULL)
|
||||
gst_element_set_state(pipeline_, GST_STATE_NULL);
|
||||
gst_element_set_state(pipeline_, GST_STATE_NULL);
|
||||
|
||||
gst_object_unref(GST_OBJECT(pipeline_));
|
||||
|
||||
@@ -213,6 +197,13 @@ void GstEnginePipeline::set_buffer_high_watermark(const double value) {
|
||||
buffer_high_watermark_ = value;
|
||||
}
|
||||
|
||||
void GstEnginePipeline::set_proxy_settings(const QString &address, const bool authentication, const QString &user, const QString &pass) {
|
||||
proxy_address_ = address;
|
||||
proxy_authentication_ = authentication;
|
||||
proxy_user_ = user;
|
||||
proxy_pass_ = pass;
|
||||
}
|
||||
|
||||
bool GstEnginePipeline::InitFromUrl(const QByteArray &stream_url, const QUrl original_url, const qint64 end_nanosec) {
|
||||
|
||||
stream_url_ = stream_url;
|
||||
@@ -228,20 +219,18 @@ bool GstEnginePipeline::InitFromUrl(const QByteArray &stream_url, const QUrl ori
|
||||
g_object_get(G_OBJECT(pipeline_), "flags", &flags, nullptr);
|
||||
flags |= 0x00000002;
|
||||
flags &= ~0x00000001;
|
||||
if (volume_enabled_) {
|
||||
flags |= 0x00000010;
|
||||
}
|
||||
else {
|
||||
flags &= ~0x00000010;
|
||||
}
|
||||
g_object_set(G_OBJECT(pipeline_), "flags", flags, nullptr);
|
||||
|
||||
pad_added_cb_id_ = CHECKED_GCONNECT(G_OBJECT(pipeline_), "pad-added", &NewPadCallback, this);
|
||||
notify_source_cb_id_ = CHECKED_GCONNECT(G_OBJECT(pipeline_), "notify::source", &SourceSetupCallback, this);
|
||||
about_to_finish_cb_id_ = CHECKED_GCONNECT(G_OBJECT(pipeline_), "about-to-finish", &AboutToFinishCallback, this);
|
||||
|
||||
// Setting up a discoverer
|
||||
discoverer_ = gst_discoverer_new(kDiscoveryTimeoutS * GST_SECOND, nullptr);
|
||||
if (discoverer_) {
|
||||
discovery_discovered_cb_id_ = CHECKED_GCONNECT(G_OBJECT(discoverer_), "discovered", &StreamDiscovered, this);
|
||||
discovery_finished_cb_id_ = CHECKED_GCONNECT(G_OBJECT(discoverer_), "finished", &StreamDiscoveryFinished, this);
|
||||
gst_discoverer_start(discoverer_);
|
||||
}
|
||||
|
||||
if (!InitAudioBin()) return false;
|
||||
|
||||
// Set playbin's sink to be our custom audio-sink.
|
||||
@@ -268,21 +257,42 @@ bool GstEnginePipeline::InitAudioBin() {
|
||||
}
|
||||
|
||||
if (device_.isValid() && g_object_class_find_property(G_OBJECT_GET_CLASS(audiosink), "device")) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
switch (device_.metaType().id()) {
|
||||
case QMetaType::QString:
|
||||
#else
|
||||
switch (device_.type()) {
|
||||
case QVariant::String:
|
||||
#endif
|
||||
if (device_.toString().isEmpty()) break;
|
||||
g_object_set(G_OBJECT(audiosink), "device", device_.toString().toUtf8().constData(), nullptr);
|
||||
break;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
case QMetaType::QByteArray:
|
||||
#else
|
||||
case QVariant::ByteArray:
|
||||
#endif
|
||||
g_object_set(G_OBJECT(audiosink), "device", device_.toByteArray().constData(), nullptr);
|
||||
break;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
case QMetaType::LongLong:
|
||||
#else
|
||||
case QVariant::LongLong:
|
||||
#endif
|
||||
g_object_set(G_OBJECT(audiosink), "device", device_.toLongLong(), nullptr);
|
||||
break;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
case QMetaType::Int:
|
||||
#else
|
||||
case QVariant::Int:
|
||||
#endif
|
||||
g_object_set(G_OBJECT(audiosink), "device", device_.toInt(), nullptr);
|
||||
break;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
case QMetaType::QUuid:
|
||||
#else
|
||||
case QVariant::Uuid:
|
||||
#endif
|
||||
g_object_set(G_OBJECT(audiosink), "device", device_.toUuid(), nullptr);
|
||||
break;
|
||||
default:
|
||||
@@ -440,13 +450,6 @@ bool GstEnginePipeline::InitAudioBin() {
|
||||
bus_cb_id_ = gst_bus_add_watch(bus, BusCallback, this);
|
||||
gst_object_unref(bus);
|
||||
|
||||
// Add request to discover the stream
|
||||
if (discoverer_) {
|
||||
if (!gst_discoverer_discover_uri_async(discoverer_, stream_url_.toStdString().c_str())) {
|
||||
qLog(Error) << "Failed to start stream discovery for" << stream_url_;
|
||||
}
|
||||
}
|
||||
|
||||
unsupported_analyzer_ = false;
|
||||
|
||||
return true;
|
||||
@@ -480,7 +483,7 @@ GstPadProbeReturn GstEnginePipeline::EventHandoffCallback(GstPad*, GstPadProbeIn
|
||||
|
||||
}
|
||||
|
||||
void GstEnginePipeline::SourceSetupCallback(GstPlayBin *bin, GParamSpec *, gpointer self) {
|
||||
void GstEnginePipeline::SourceSetupCallback(GstPlayBin *bin, GParamSpec*, gpointer self) {
|
||||
|
||||
GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self);
|
||||
|
||||
@@ -502,6 +505,19 @@ void GstEnginePipeline::SourceSetupCallback(GstPlayBin *bin, GParamSpec *, gpoin
|
||||
g_object_set(element, "ssl-strict", FALSE, nullptr);
|
||||
}
|
||||
|
||||
if (!instance->proxy_address_.isEmpty() && g_object_class_find_property(G_OBJECT_GET_CLASS(element), "proxy")) {
|
||||
qLog(Debug) << "Setting proxy to" << instance->proxy_address_;
|
||||
g_object_set(element, "proxy", instance->proxy_address_.toUtf8().constData(), nullptr);
|
||||
if (instance->proxy_authentication_ &&
|
||||
g_object_class_find_property(G_OBJECT_GET_CLASS(element), "proxy-id") &&
|
||||
g_object_class_find_property(G_OBJECT_GET_CLASS(element), "proxy-pw") &&
|
||||
!instance->proxy_user_.isEmpty() &&
|
||||
!instance->proxy_pass_.isEmpty())
|
||||
{
|
||||
g_object_set(element, "proxy-id", instance->proxy_user_.toUtf8().constData(), "proxy-pw", instance->proxy_pass_.toUtf8().constData(), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// If the pipeline was buffering we stop that now.
|
||||
if (instance->buffering_) {
|
||||
instance->buffering_ = false;
|
||||
@@ -537,7 +553,7 @@ void GstEnginePipeline::NewPadCallback(GstElement*, GstPad *pad, gpointer self)
|
||||
gst_pad_add_probe(pad, static_cast<GstPadProbeType>(GST_PAD_PROBE_TYPE_BUFFER | GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM | GST_PAD_PROBE_TYPE_EVENT_FLUSH), PlaybinProbe, instance, nullptr);
|
||||
|
||||
instance->pipeline_is_connected_ = true;
|
||||
if (instance->pending_seek_nanosec_ != -1 && instance->pipeline_is_initialised_) {
|
||||
if (instance->pending_seek_nanosec_ != -1 && instance->pipeline_is_initialized_) {
|
||||
QMetaObject::invokeMethod(instance, "Seek", Qt::QueuedConnection, Q_ARG(qint64, instance->pending_seek_nanosec_));
|
||||
}
|
||||
|
||||
@@ -594,6 +610,7 @@ GstPadProbeReturn GstEnginePipeline::HandoffCallback(GstPad *pad, GstPadProbeInf
|
||||
int rate = 0;
|
||||
gst_structure_get_int(structure, "channels", &channels);
|
||||
gst_structure_get_int(structure, "rate", &rate);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
GstBuffer *buf = gst_pad_probe_info_get_buffer(info);
|
||||
GstBuffer *buf16 = nullptr;
|
||||
@@ -731,8 +748,6 @@ gboolean GstEnginePipeline::BusCallback(GstBus*, GstMessage *msg, gpointer self)
|
||||
|
||||
GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self);
|
||||
|
||||
qLog(Debug) << instance->id() << "bus message" << GST_MESSAGE_TYPE_NAME(msg);
|
||||
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_ERROR:
|
||||
instance->ErrorMessageReceived(msg);
|
||||
@@ -758,8 +773,6 @@ GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage *msg, gpo
|
||||
|
||||
GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self);
|
||||
|
||||
qLog(Debug) << instance->id() << "sync bus message" << GST_MESSAGE_TYPE_NAME(msg);
|
||||
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_EOS:
|
||||
emit instance->EndOfStreamReached(instance->id(), false);
|
||||
@@ -804,7 +817,7 @@ GstBusSyncReply GstEnginePipeline::BusCallbackSync(GstBus*, GstMessage *msg, gpo
|
||||
void GstEnginePipeline::StreamStatusMessageReceived(GstMessage *msg) {
|
||||
|
||||
GstStreamStatusType type;
|
||||
GstElement *owner;
|
||||
GstElement *owner = nullptr;
|
||||
gst_message_parse_stream_status(msg, &type, &owner);
|
||||
|
||||
if (type == GST_STREAM_STATUS_TYPE_CREATE) {
|
||||
@@ -835,9 +848,9 @@ void GstEnginePipeline::StreamStartMessageReceived() {
|
||||
|
||||
}
|
||||
|
||||
void GstEnginePipeline::TaskEnterCallback(GstTask *, GThread *, gpointer) {
|
||||
void GstEnginePipeline::TaskEnterCallback(GstTask*, GThread*, gpointer) {
|
||||
|
||||
// Bump the priority of the thread only on OS X
|
||||
// Bump the priority of the thread only on macOS
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
sched_param param;
|
||||
@@ -875,13 +888,13 @@ void GstEnginePipeline::ErrorMessageReceived(GstMessage *msg) {
|
||||
g_error_free(error);
|
||||
g_free(debugs);
|
||||
|
||||
if (state() == GST_STATE_PLAYING && pipeline_is_initialised_ && next_uri_set_ && (domain == GST_RESOURCE_ERROR || domain == GST_STREAM_ERROR)) {
|
||||
if (pipeline_is_initialized_ && next_uri_set_ && (domain == GST_RESOURCE_ERROR || domain == GST_STREAM_ERROR)) {
|
||||
// A track is still playing and the next uri is not playable. We ignore the error here so it can play until the end.
|
||||
// But there is no message send to the bus when the current track finishes, we have to add an EOS ourself.
|
||||
qLog(Info) << "Ignoring error when loading next track";
|
||||
GstPad *sinkpad = gst_element_get_static_pad(audiobin_, "sink");
|
||||
gst_pad_send_event(sinkpad, gst_event_new_eos());
|
||||
gst_object_unref(sinkpad);
|
||||
GstPad *pad = gst_element_get_static_pad(audiobin_, "sink");
|
||||
gst_pad_send_event(pad, gst_event_new_eos());
|
||||
gst_object_unref(pad);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -949,7 +962,7 @@ void GstEnginePipeline::TagMessageReceived(GstMessage *msg) {
|
||||
}
|
||||
}
|
||||
|
||||
gst_tag_list_free(taglist);
|
||||
gst_tag_list_unref(taglist);
|
||||
|
||||
emit MetadataFound(id(), bundle);
|
||||
|
||||
@@ -990,29 +1003,21 @@ void GstEnginePipeline::StateChangedMessageReceived(GstMessage *msg) {
|
||||
GstState old_state, new_state, pending;
|
||||
gst_message_parse_state_changed(msg, &old_state, &new_state, &pending);
|
||||
|
||||
if (!pipeline_is_initialised_ && (new_state == GST_STATE_PAUSED || new_state == GST_STATE_PLAYING)) {
|
||||
pipeline_is_initialised_ = true;
|
||||
if (!pipeline_is_initialized_ && (new_state == GST_STATE_PAUSED || new_state == GST_STATE_PLAYING)) {
|
||||
pipeline_is_initialized_ = true;
|
||||
if (pending_seek_nanosec_ != -1 && pipeline_is_connected_) {
|
||||
QMetaObject::invokeMethod(this, "Seek", Qt::QueuedConnection, Q_ARG(qint64, pending_seek_nanosec_));
|
||||
}
|
||||
}
|
||||
|
||||
if (pipeline_is_initialised_ && new_state != GST_STATE_PAUSED && new_state != GST_STATE_PLAYING) {
|
||||
pipeline_is_initialised_ = false;
|
||||
if (pipeline_is_initialized_ && new_state != GST_STATE_PAUSED && new_state != GST_STATE_PLAYING) {
|
||||
pipeline_is_initialized_ = false;
|
||||
|
||||
if (next_uri_set_ && new_state == GST_STATE_READY) {
|
||||
// Revert uri and go back to PLAY state again
|
||||
next_uri_set_ = false;
|
||||
g_object_set(G_OBJECT(pipeline_), "uri", stream_url_.constData(), nullptr);
|
||||
SetState(GST_STATE_PLAYING);
|
||||
|
||||
// Add request to discover the stream
|
||||
if (discoverer_) {
|
||||
if (!gst_discoverer_discover_uri_async(discoverer_, stream_url_.toStdString().c_str())) {
|
||||
qLog(Error) << "Failed to start stream discovery for" << stream_url_;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,7 +1055,7 @@ void GstEnginePipeline::BufferingMessageReceived(GstMessage *msg) {
|
||||
|
||||
qint64 GstEnginePipeline::position() const {
|
||||
|
||||
if (pipeline_is_initialised_)
|
||||
if (pipeline_is_initialized_)
|
||||
gst_element_query_position(pipeline_, GST_FORMAT_TIME, &last_known_position_ns_);
|
||||
|
||||
return last_known_position_ns_;
|
||||
@@ -1077,7 +1082,7 @@ GstState GstEnginePipeline::state() const {
|
||||
}
|
||||
|
||||
QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(const GstState state) {
|
||||
return ConcurrentRun::Run<GstStateChangeReturn, GstElement*, GstState>(&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
|
||||
return QtConcurrent::run(&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
|
||||
}
|
||||
|
||||
bool GstEnginePipeline::Seek(const qint64 nanosec) {
|
||||
@@ -1087,7 +1092,7 @@ bool GstEnginePipeline::Seek(const qint64 nanosec) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!pipeline_is_connected_ || !pipeline_is_initialised_) {
|
||||
if (!pipeline_is_connected_ || !pipeline_is_initialized_) {
|
||||
pending_seek_nanosec_ = nanosec;
|
||||
return true;
|
||||
}
|
||||
@@ -1261,76 +1266,4 @@ void GstEnginePipeline::SetNextUrl(const QByteArray &stream_url, const QUrl &ori
|
||||
next_beginning_offset_nanosec_ = beginning_nanosec;
|
||||
next_end_offset_nanosec_ = end_nanosec;
|
||||
|
||||
// Add request to discover the stream
|
||||
if (discoverer_) {
|
||||
if (!gst_discoverer_discover_uri_async(discoverer_, next_stream_url_.toStdString().c_str())) {
|
||||
qLog(Error) << "Failed to start stream discovery for" << next_stream_url_;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GstEnginePipeline::StreamDiscovered(GstDiscoverer*, GstDiscovererInfo *info, GError*, gpointer self) {
|
||||
|
||||
GstEnginePipeline *instance = reinterpret_cast<GstEnginePipeline*>(self);
|
||||
|
||||
QString discovered_url(gst_discoverer_info_get_uri(info));
|
||||
|
||||
GstDiscovererResult result = gst_discoverer_info_get_result(info);
|
||||
if (result != GST_DISCOVERER_OK) {
|
||||
QString error_message = GSTdiscovererErrorMessage(result);
|
||||
qLog(Error) << QString("Stream discovery for %1 failed: %2").arg(discovered_url).arg(error_message);
|
||||
return;
|
||||
}
|
||||
|
||||
GList *audio_streams = gst_discoverer_info_get_audio_streams(info);
|
||||
if (audio_streams) {
|
||||
|
||||
GstDiscovererStreamInfo *stream_info = reinterpret_cast<GstDiscovererStreamInfo*>(g_list_first(audio_streams)->data);
|
||||
|
||||
Engine::SimpleMetaBundle bundle;
|
||||
if (discovered_url == instance->stream_url_) {
|
||||
bundle.url = instance->original_url_;
|
||||
}
|
||||
else if (discovered_url == instance->next_stream_url_) {
|
||||
bundle.url = instance->next_original_url_;
|
||||
}
|
||||
bundle.stream_url = QUrl(discovered_url);
|
||||
bundle.samplerate = gst_discoverer_audio_info_get_sample_rate(GST_DISCOVERER_AUDIO_INFO(stream_info));
|
||||
bundle.bitdepth = gst_discoverer_audio_info_get_depth(GST_DISCOVERER_AUDIO_INFO(stream_info));
|
||||
bundle.bitrate = gst_discoverer_audio_info_get_bitrate(GST_DISCOVERER_AUDIO_INFO(stream_info)) / 1000;
|
||||
|
||||
GstCaps *caps = gst_discoverer_stream_info_get_caps(stream_info);
|
||||
gchar *codec_description = gst_pb_utils_get_codec_description(caps);
|
||||
QString filetype_description = (codec_description ? QString(codec_description) : QString("Unknown"));
|
||||
g_free(codec_description);
|
||||
|
||||
gst_caps_unref(caps);
|
||||
gst_discoverer_stream_info_list_free(audio_streams);
|
||||
|
||||
bundle.filetype = Song::FiletypeByDescription(filetype_description);
|
||||
qLog(Info) << "Got stream info for" << discovered_url + ":" << filetype_description;
|
||||
|
||||
emit instance->MetadataFound(instance->id(), bundle);
|
||||
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Could not detect an audio stream in" << discovered_url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GstEnginePipeline::StreamDiscoveryFinished(GstDiscoverer*, gpointer) {}
|
||||
|
||||
QString GstEnginePipeline::GSTdiscovererErrorMessage(GstDiscovererResult result) {
|
||||
|
||||
switch (result) {
|
||||
case GST_DISCOVERER_URI_INVALID: return "The URI is invalid";
|
||||
case GST_DISCOVERER_TIMEOUT: return "The discovery timed-out";
|
||||
case GST_DISCOVERER_BUSY: return "The discoverer was already discovering a file";
|
||||
case GST_DISCOVERER_MISSING_PLUGINS: return "Some plugins are missing for full discovery";
|
||||
case GST_DISCOVERER_ERROR:
|
||||
default: return "An error happened and the GError is set";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
#include <glib-object.h>
|
||||
#include <glib/gtypes.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/pbutils/pbutils.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
@@ -74,6 +73,7 @@ class GstEnginePipeline : public QObject {
|
||||
void set_buffer_duration_nanosec(const qint64 duration_nanosec);
|
||||
void set_buffer_low_watermark(const double value);
|
||||
void set_buffer_high_watermark(const double value);
|
||||
void set_proxy_settings(const QString &address, const bool authentication, const QString &user, const QString &pass);
|
||||
|
||||
// Creates the pipeline, returns false on error
|
||||
bool InitFromUrl(const QByteArray &stream_url, const QUrl original_url, const qint64 end_nanosec);
|
||||
@@ -100,7 +100,9 @@ class GstEnginePipeline : public QObject {
|
||||
|
||||
// Get information about the music playback
|
||||
QByteArray stream_url() const { return stream_url_; }
|
||||
QByteArray next_stream_url() const { return next_stream_url_; }
|
||||
QUrl original_url() const { return original_url_; }
|
||||
QUrl next_original_url() const { return next_original_url_; }
|
||||
bool is_valid() const { return valid_; }
|
||||
|
||||
// Please note that this method (unlike GstEngine's.position()) is multiple-section media unaware.
|
||||
@@ -149,9 +151,6 @@ class GstEnginePipeline : public QObject {
|
||||
static GstBusSyncReply BusCallbackSync(GstBus*, GstMessage*, gpointer);
|
||||
static gboolean BusCallback(GstBus*, GstMessage*, gpointer);
|
||||
static void TaskEnterCallback(GstTask*, GThread*, gpointer);
|
||||
static void StreamDiscovered(GstDiscoverer*, GstDiscovererInfo *info, GError*, gpointer self);
|
||||
static void StreamDiscoveryFinished(GstDiscoverer*, gpointer);
|
||||
static QString GSTdiscovererErrorMessage(GstDiscovererResult result);
|
||||
|
||||
void TagMessageReceived(GstMessage*);
|
||||
void ErrorMessageReceived(GstMessage*);
|
||||
@@ -174,7 +173,6 @@ class GstEnginePipeline : public QObject {
|
||||
private:
|
||||
static const int kGstStateTimeoutNanosecs;
|
||||
static const int kFaderFudgeMsec;
|
||||
static const int kDiscoveryTimeoutS;
|
||||
static const int kEqBandCount;
|
||||
static const int kEqBandFrequencies[];
|
||||
|
||||
@@ -216,6 +214,12 @@ class GstEnginePipeline : public QObject {
|
||||
double buffer_high_watermark_;
|
||||
bool buffering_;
|
||||
|
||||
// Proxy
|
||||
QString proxy_address_;
|
||||
bool proxy_authentication_;
|
||||
QString proxy_user_;
|
||||
QString proxy_pass_;
|
||||
|
||||
// These get called when there is a new audio buffer available
|
||||
QList<GstBufferConsumer*> buffer_consumers_;
|
||||
QMutex buffer_consumers_mutex_;
|
||||
@@ -249,7 +253,7 @@ class GstEnginePipeline : public QObject {
|
||||
|
||||
// Seeking while the pipeline is in the READY state doesn't work, so we have to wait until it goes to PAUSED or PLAYING.
|
||||
// Also we have to wait for the playbin to be connected.
|
||||
bool pipeline_is_initialised_;
|
||||
bool pipeline_is_initialized_;
|
||||
bool pipeline_is_connected_;
|
||||
qint64 pending_seek_nanosec_;
|
||||
|
||||
@@ -275,14 +279,11 @@ class GstEnginePipeline : public QObject {
|
||||
GstElement *audiopanorama_;
|
||||
GstElement *equalizer_;
|
||||
GstElement *equalizer_preamp_;
|
||||
GstDiscoverer *discoverer_;
|
||||
|
||||
int pad_added_cb_id_;
|
||||
int notify_source_cb_id_;
|
||||
int about_to_finish_cb_id_;
|
||||
int bus_cb_id_;
|
||||
int discovery_finished_cb_id_;
|
||||
int discovery_discovered_cb_id_;
|
||||
|
||||
QThreadPool set_state_threadpool_;
|
||||
|
||||
|
||||
@@ -41,14 +41,16 @@
|
||||
#include "gststartup.h"
|
||||
|
||||
GstStartup::GstStartup(QObject *parent) : QObject(parent) {
|
||||
initialising_ = QtConcurrent::run([=]{ InitialiseGStreamer(); });
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
initializing_ = QtConcurrent::run(&GstStartup::InitializeGStreamer, this);
|
||||
#else
|
||||
initializing_ = QtConcurrent::run(this, &GstStartup::InitializeGStreamer);
|
||||
#endif
|
||||
}
|
||||
|
||||
GstStartup::~GstStartup() {
|
||||
//gst_deinit();
|
||||
}
|
||||
GstStartup::~GstStartup() {}
|
||||
|
||||
void GstStartup::InitialiseGStreamer() {
|
||||
void GstStartup::InitializeGStreamer() {
|
||||
|
||||
SetEnvironment();
|
||||
|
||||
|
||||
@@ -34,12 +34,12 @@ class GstStartup : public QObject {
|
||||
explicit GstStartup(QObject *parent = nullptr);
|
||||
~GstStartup() override;
|
||||
|
||||
void EnsureInitialised() { initialising_.waitForFinished(); }
|
||||
void EnsureInitialized() { initializing_.waitForFinished(); }
|
||||
|
||||
private:
|
||||
void InitialiseGStreamer();
|
||||
void InitializeGStreamer();
|
||||
void SetEnvironment();
|
||||
QFuture<void> initialising_;
|
||||
QFuture<void> initializing_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class MacOsDeviceFinder : public DeviceFinder {
|
||||
public:
|
||||
explicit MacOsDeviceFinder();
|
||||
|
||||
virtual bool Initialise() { return true; }
|
||||
virtual bool Initialize() { return true; }
|
||||
virtual QList<Device> ListDevices();
|
||||
};
|
||||
|
||||
|
||||
@@ -36,28 +36,7 @@ MMDeviceFinder::MMDeviceFinder() : DeviceFinder("mmdevice", { "wasapisink" }) {}
|
||||
|
||||
QList<DeviceFinder::Device> MMDeviceFinder::ListDevices() {
|
||||
|
||||
HRESULT hr = S_OK;
|
||||
|
||||
IMMDeviceEnumerator *enumerator = nullptr;
|
||||
hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)&enumerator);
|
||||
if (FAILED(hr)) {
|
||||
return QList<Device>();
|
||||
}
|
||||
|
||||
IMMDeviceCollection *collection = nullptr;
|
||||
hr = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);
|
||||
if (FAILED(hr)) {
|
||||
enumerator->Release();
|
||||
return QList<Device>();
|
||||
}
|
||||
|
||||
UINT count;
|
||||
hr = collection->GetCount(&count);
|
||||
if (FAILED(hr)) {
|
||||
collection->Release();
|
||||
enumerator->Release();
|
||||
return QList<Device>();
|
||||
}
|
||||
HRESULT hr_coinit = CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
||||
|
||||
QList<Device> devices;
|
||||
Device default_device;
|
||||
@@ -65,51 +44,71 @@ QList<DeviceFinder::Device> MMDeviceFinder::ListDevices() {
|
||||
default_device.iconname = GuessIconName(default_device.description);
|
||||
devices.append(default_device);
|
||||
|
||||
for (ULONG i = 0 ; i < count ; i++) {
|
||||
|
||||
IMMDevice *endpoint = nullptr;
|
||||
hr = collection->Item(i, &endpoint);
|
||||
if (FAILED(hr)) { return devices; }
|
||||
|
||||
LPWSTR pwszid = nullptr;
|
||||
hr = endpoint->GetId(&pwszid);
|
||||
if (FAILED(hr)) {
|
||||
endpoint->Release();
|
||||
continue;
|
||||
IMMDeviceEnumerator *enumerator = nullptr;
|
||||
HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)&enumerator);
|
||||
if (hr == S_OK) {
|
||||
IMMDeviceCollection *collection = nullptr;
|
||||
hr = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &collection);
|
||||
if (hr == S_OK) {
|
||||
UINT count;
|
||||
hr = collection->GetCount(&count);
|
||||
if (hr == S_OK) {
|
||||
for (ULONG i = 0 ; i < count ; i++) {
|
||||
IMMDevice *endpoint = nullptr;
|
||||
hr = collection->Item(i, &endpoint);
|
||||
if (hr == S_OK) {
|
||||
LPWSTR pwszid = nullptr;
|
||||
hr = endpoint->GetId(&pwszid);
|
||||
if (hr == S_OK) {
|
||||
IPropertyStore *props = nullptr;
|
||||
hr = endpoint->OpenPropertyStore(STGM_READ, &props);
|
||||
if (hr == S_OK) {
|
||||
PROPVARIANT var_name;
|
||||
PropVariantInit(&var_name);
|
||||
hr = props->GetValue(PKEY_Device_FriendlyName, &var_name);
|
||||
if (hr == S_OK) {
|
||||
Device device;
|
||||
device.description = QString::fromWCharArray(var_name.pwszVal);
|
||||
device.iconname = GuessIconName(device.description);
|
||||
device.value = QString::fromStdWString(pwszid);
|
||||
devices.append(device);
|
||||
PropVariantClear(&var_name);
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "IPropertyStore::GetValue failed." << Qt::hex << DWORD(hr);
|
||||
}
|
||||
props->Release();
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "IPropertyStore::OpenPropertyStore failed." << Qt::hex << DWORD(hr);
|
||||
}
|
||||
CoTaskMemFree(pwszid);
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "IMMDevice::GetId failed." << Qt::hex << DWORD(hr);
|
||||
}
|
||||
endpoint->Release();
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "IMMDeviceCollection::Item failed." << Qt::hex << DWORD(hr);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "IMMDeviceCollection::GetCount failed." << Qt::hex << DWORD(hr);
|
||||
}
|
||||
collection->Release();
|
||||
}
|
||||
|
||||
IPropertyStore *props = nullptr;
|
||||
hr = endpoint->OpenPropertyStore(STGM_READ, &props);
|
||||
if (FAILED(hr)) {
|
||||
CoTaskMemFree(pwszid);
|
||||
endpoint->Release();
|
||||
continue;
|
||||
else {
|
||||
qLog(Error) << "EnumAudioEndpoints failed." << Qt::hex << DWORD(hr);
|
||||
}
|
||||
|
||||
PROPVARIANT var_name;
|
||||
PropVariantInit(&var_name);
|
||||
hr = props->GetValue(PKEY_Device_FriendlyName, &var_name);
|
||||
if (FAILED(hr)) {
|
||||
props->Release();
|
||||
CoTaskMemFree(pwszid);
|
||||
endpoint->Release();
|
||||
continue;
|
||||
}
|
||||
|
||||
Device device;
|
||||
device.description = QString::fromWCharArray(var_name.pwszVal);
|
||||
device.iconname = GuessIconName(device.description);
|
||||
device.value = QString::fromStdWString(pwszid);
|
||||
devices.append(device);
|
||||
|
||||
PropVariantClear(&var_name);
|
||||
props->Release();
|
||||
CoTaskMemFree(pwszid);
|
||||
endpoint->Release();
|
||||
|
||||
enumerator->Release();
|
||||
}
|
||||
collection->Release();
|
||||
enumerator->Release();
|
||||
else {
|
||||
qLog(Error) << "CoCreateInstance failed." << Qt::hex << DWORD(hr);
|
||||
}
|
||||
|
||||
if (hr_coinit == S_OK || hr_coinit == S_FALSE) CoUninitialize();
|
||||
|
||||
return devices;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class MMDeviceFinder : public DeviceFinder {
|
||||
public:
|
||||
explicit MMDeviceFinder();
|
||||
|
||||
virtual bool Initialise() { return true; }
|
||||
virtual bool Initialize() { return true; }
|
||||
virtual QList<Device> ListDevices();
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
PulseDeviceFinder::PulseDeviceFinder() : DeviceFinder("pulseaudio", {"pulseaudio", "pulse", "pulsesink"} ), mainloop_(nullptr), context_(nullptr) {
|
||||
}
|
||||
|
||||
bool PulseDeviceFinder::Initialise() {
|
||||
bool PulseDeviceFinder::Initialize() {
|
||||
|
||||
mainloop_ = pa_mainloop_new();
|
||||
if (!mainloop_) {
|
||||
|
||||
@@ -36,7 +36,7 @@ class PulseDeviceFinder : public DeviceFinder {
|
||||
explicit PulseDeviceFinder();
|
||||
~PulseDeviceFinder() override;
|
||||
|
||||
bool Initialise() override;
|
||||
bool Initialize() override;
|
||||
QList<Device> ListDevices() override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <vlc/vlc.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QMetaType>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
@@ -108,7 +109,7 @@ bool VLCEngine::Load(const QUrl &stream_url, const QUrl &original_url, const Eng
|
||||
Q_UNUSED(beginning_nanosec);
|
||||
Q_UNUSED(end_nanosec);
|
||||
|
||||
if (!Initialised()) return false;
|
||||
if (!Initialized()) return false;
|
||||
|
||||
// Create the media object
|
||||
VlcScopedRef<libvlc_media_t> media(libvlc_media_new_location(instance_, stream_url.toEncoded().constData()));
|
||||
@@ -121,7 +122,7 @@ bool VLCEngine::Load(const QUrl &stream_url, const QUrl &original_url, const Eng
|
||||
|
||||
bool VLCEngine::Play(const quint64 offset_nanosec) {
|
||||
|
||||
if (!Initialised()) return false;
|
||||
if (!Initialized()) return false;
|
||||
|
||||
// Set audio output
|
||||
if (!output_.isEmpty() && output_ != "auto") {
|
||||
@@ -130,7 +131,13 @@ bool VLCEngine::Play(const quint64 offset_nanosec) {
|
||||
}
|
||||
|
||||
// Set audio device
|
||||
if (device_.isValid() && device_.type() == QVariant::String && !device_.toString().isEmpty()) {
|
||||
if (device_.isValid() &&
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
device_.metaType().id() == QMetaType::QString
|
||||
#else
|
||||
device_.type() == QVariant::String
|
||||
#endif
|
||||
&& !device_.toString().isEmpty()) {
|
||||
libvlc_audio_output_device_set(player_, nullptr, device_.toString().toLocal8Bit().data());
|
||||
}
|
||||
|
||||
@@ -147,28 +154,28 @@ void VLCEngine::Stop(const bool stop_after) {
|
||||
|
||||
Q_UNUSED(stop_after);
|
||||
|
||||
if (!Initialised()) return;
|
||||
if (!Initialized()) return;
|
||||
libvlc_media_player_stop(player_);
|
||||
|
||||
}
|
||||
|
||||
void VLCEngine::Pause() {
|
||||
|
||||
if (!Initialised()) return;
|
||||
if (!Initialized()) return;
|
||||
libvlc_media_player_pause(player_);
|
||||
|
||||
}
|
||||
|
||||
void VLCEngine::Unpause() {
|
||||
|
||||
if (!Initialised()) return;
|
||||
if (!Initialized()) return;
|
||||
libvlc_media_player_play(player_);
|
||||
|
||||
}
|
||||
|
||||
void VLCEngine::Seek(const quint64 offset_nanosec) {
|
||||
|
||||
if (!Initialised()) return;
|
||||
if (!Initialized()) return;
|
||||
|
||||
int offset = (offset_nanosec / kNsecPerMsec);
|
||||
|
||||
@@ -182,7 +189,7 @@ void VLCEngine::Seek(const quint64 offset_nanosec) {
|
||||
}
|
||||
|
||||
void VLCEngine::SetVolumeSW(const uint percent) {
|
||||
if (!Initialised()) return;
|
||||
if (!Initialized()) return;
|
||||
if (!volume_control_ && percent != 100) return;
|
||||
libvlc_audio_set_volume(player_, percent);
|
||||
}
|
||||
@@ -248,7 +255,7 @@ bool VLCEngine::ALSADeviceSupport(const QString &output) {
|
||||
|
||||
uint VLCEngine::position() const {
|
||||
|
||||
if (!Initialised()) return (0);
|
||||
if (!Initialized()) return (0);
|
||||
|
||||
bool is_playing = libvlc_media_player_is_playing(player_);
|
||||
if (!is_playing) return 0;
|
||||
@@ -260,7 +267,7 @@ uint VLCEngine::position() const {
|
||||
|
||||
uint VLCEngine::length() const {
|
||||
|
||||
if (!Initialised()) return(0);
|
||||
if (!Initialized()) return(0);
|
||||
|
||||
bool is_playing = libvlc_media_player_is_playing(player_);
|
||||
if (!is_playing) return 0;
|
||||
|
||||
@@ -72,7 +72,7 @@ class VLCEngine : public Engine::Base {
|
||||
libvlc_media_player_t *player_;
|
||||
Engine::State state_;
|
||||
|
||||
bool Initialised() const { return (instance_ && player_); }
|
||||
bool Initialized() const { return (instance_ && player_); }
|
||||
uint position() const;
|
||||
uint length() const;
|
||||
bool CanDecode(const QUrl &url);
|
||||
|
||||
202
src/globalshortcuts/globalshortcutbackend-kde.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <dbus/kglobalaccel.h>
|
||||
#include <dbus/kglobalaccelcomponent.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QAction>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusObjectPath>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QKeySequence>
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
# include <QKeyCombination>
|
||||
#endif
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/closure.h"
|
||||
|
||||
#include "globalshortcutbackend-kde.h"
|
||||
|
||||
const char *GlobalShortcutBackendKDE::kKdeService = "org.kde.kglobalaccel";
|
||||
const char *GlobalShortcutBackendKDE::kKdePath = "/kglobalaccel";
|
||||
|
||||
GlobalShortcutBackendKDE::GlobalShortcutBackendKDE(GlobalShortcuts *parent) : GlobalShortcutBackend(parent), interface_(nullptr), component_(nullptr) {}
|
||||
|
||||
bool GlobalShortcutBackendKDE::DoRegister() {
|
||||
|
||||
qLog(Debug) << "Registering";
|
||||
|
||||
if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(kKdeService)) {
|
||||
qLog(Warning) << "KGlobalAccel is not registered";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!interface_) {
|
||||
interface_ = new OrgKdeKGlobalAccelInterface(kKdeService, kKdePath, QDBusConnection::sessionBus(), this);
|
||||
}
|
||||
|
||||
for (const GlobalShortcuts::Shortcut &shortcut : manager_->shortcuts().values()) {
|
||||
RegisterShortcut(shortcut);
|
||||
}
|
||||
|
||||
QDBusPendingReply<QDBusObjectPath> reply = interface_->getComponent(QCoreApplication::applicationName());
|
||||
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
|
||||
NewClosure(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(RegisterFinished(QDBusPendingCallWatcher*)), watcher);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void GlobalShortcutBackendKDE::RegisterFinished(QDBusPendingCallWatcher *watcher) {
|
||||
|
||||
QDBusReply<QDBusObjectPath> reply = watcher->reply();
|
||||
watcher->deleteLater();
|
||||
|
||||
if (!reply.isValid()) {
|
||||
if (reply.error().name() != "org.kde.kglobalaccel.NoSuchComponent") {
|
||||
qLog(Error) << "Failed to register:" << reply.error().name() << reply.error().message();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!component_) {
|
||||
component_ = new org::kde::kglobalaccel::Component(kKdeService, reply.value().path(), QDBusConnection::sessionBus(), interface_);
|
||||
}
|
||||
|
||||
if (!component_->isValid()) {
|
||||
qLog(Error) << "Component is invalid:" << QDBusConnection::sessionBus().lastError();
|
||||
return;
|
||||
}
|
||||
|
||||
connect(component_, SIGNAL(globalShortcutPressed(QString, QString, qlonglong)), this, SLOT(GlobalShortcutPressed(QString, QString, qlonglong)), Qt::UniqueConnection);
|
||||
|
||||
qLog(Debug) << "Registered";
|
||||
|
||||
}
|
||||
|
||||
void GlobalShortcutBackendKDE::DoUnregister() {
|
||||
|
||||
if (!interface_ || !interface_->isValid()) return;
|
||||
|
||||
qLog(Debug) << "Unregistering";
|
||||
|
||||
for (const GlobalShortcuts::Shortcut &shortcut : manager_->shortcuts()) {
|
||||
if (actions_.contains(shortcut.id)) {
|
||||
interface_->unRegister(GetActionId(shortcut.id, shortcut.action));
|
||||
actions_.remove(shortcut.id, shortcut.action);
|
||||
qLog(Info) << "Unregistered shortcut" << shortcut.id << shortcut.action->shortcut();
|
||||
}
|
||||
}
|
||||
|
||||
if (component_) disconnect(component_, nullptr, this, nullptr);
|
||||
|
||||
qLog(Debug) << "Unregistered";
|
||||
|
||||
}
|
||||
|
||||
bool GlobalShortcutBackendKDE::RegisterShortcut(const GlobalShortcuts::Shortcut &shortcut) {
|
||||
|
||||
if (!interface_ || !interface_->isValid() || shortcut.id.isEmpty() || !shortcut.action || shortcut.action->shortcut().isEmpty()) return false;
|
||||
|
||||
if (shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaPlay) ||
|
||||
shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaStop) ||
|
||||
shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaNext) ||
|
||||
shortcut.action->shortcut() == QKeySequence(Qt::Key_MediaPrevious)) {
|
||||
qLog(Info) << "Media shortcut" << shortcut.id << shortcut.action->shortcut();
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList action_id = GetActionId(shortcut.id, shortcut.action);
|
||||
actions_.insert(shortcut.id, shortcut.action);
|
||||
interface_->doRegister(action_id);
|
||||
|
||||
QList<QKeySequence> active_shortcut = QList<QKeySequence>() << shortcut.action->shortcut();
|
||||
|
||||
const QList<int> result = interface_->setShortcut(action_id, ToIntList(active_shortcut), 0x2);
|
||||
const QList<QKeySequence> result_sequence = ToKeySequenceList(result);
|
||||
if (result_sequence != active_shortcut) {
|
||||
if (result_sequence.isEmpty()) {
|
||||
shortcut.action->setShortcut(QKeySequence());
|
||||
}
|
||||
else {
|
||||
shortcut.action->setShortcut(result_sequence[0]);
|
||||
}
|
||||
}
|
||||
|
||||
qLog(Info) << "Registered shortcut" << shortcut.id << shortcut.action->shortcut();
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
QStringList GlobalShortcutBackendKDE::GetActionId(const QString &id, const QAction *action) {
|
||||
|
||||
QStringList ret;
|
||||
ret << QCoreApplication::applicationName();
|
||||
ret << id;
|
||||
ret << QCoreApplication::applicationName();
|
||||
ret << action->text().remove('&');
|
||||
if (ret.back().isEmpty()) ret.back() = id;
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QList<int> GlobalShortcutBackendKDE::ToIntList(const QList<QKeySequence> &sequence_list) {
|
||||
|
||||
QList<int> ret;
|
||||
for (const QKeySequence &sequence : sequence_list) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
ret.append(sequence[0].toCombined());
|
||||
#else
|
||||
ret.append(sequence[0]);
|
||||
#endif
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QList<QKeySequence> GlobalShortcutBackendKDE::ToKeySequenceList(const QList<int> &sequence_list) {
|
||||
|
||||
QList<QKeySequence> ret;
|
||||
for (int sequence : sequence_list) {
|
||||
ret.append(sequence);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void GlobalShortcutBackendKDE::GlobalShortcutPressed(const QString &component_unique, const QString &shortcut_unique, qlonglong) {
|
||||
|
||||
if (QCoreApplication::applicationName() == component_unique && actions_.contains(shortcut_unique)) {
|
||||
for (QAction *action : actions_.values(shortcut_unique)) {
|
||||
qLog(Debug) << "Key" << action->shortcut() << "pressed.";
|
||||
if (action->isEnabled()) action->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
70
src/globalshortcuts/globalshortcutbackend-kde.h
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GLOBALSHORTCUTBACKEND_KDE_H
|
||||
#define GLOBALSHORTCUTBACKEND_KDE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMultiHash>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QKeySequence>
|
||||
|
||||
#include "globalshortcutbackend.h"
|
||||
#include "globalshortcuts.h"
|
||||
|
||||
class QDBusPendingCallWatcher;
|
||||
class QAction;
|
||||
|
||||
class OrgKdeKGlobalAccelInterface;
|
||||
class OrgKdeKglobalaccelComponentInterface;
|
||||
|
||||
class GlobalShortcutBackendKDE : public GlobalShortcutBackend {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GlobalShortcutBackendKDE(GlobalShortcuts *parent);
|
||||
|
||||
static const char *kKdeService;
|
||||
|
||||
protected:
|
||||
bool DoRegister() override;
|
||||
void DoUnregister() override;
|
||||
|
||||
private:
|
||||
bool RegisterShortcut(const GlobalShortcuts::Shortcut &shortcut);
|
||||
static QStringList GetActionId(const QString &id, const QAction *action);
|
||||
static QList<int> ToIntList(const QList<QKeySequence> &sequence_list);
|
||||
static QList<QKeySequence> ToKeySequenceList(const QList<int> &sequence_list);
|
||||
|
||||
private slots:
|
||||
void RegisterFinished(QDBusPendingCallWatcher *watcher);
|
||||
void GlobalShortcutPressed(const QString &component_unique, const QString &shortcut_unique, qlonglong);
|
||||
|
||||
private:
|
||||
static const char *kKdePath;
|
||||
|
||||
OrgKdeKGlobalAccelInterface *interface_;
|
||||
OrgKdeKglobalaccelComponentInterface *component_;
|
||||
QMultiHash<QString, QAction*> actions_;
|
||||
};
|
||||
|
||||
#endif // GLOBALSHORTCUTBACKEND_KDE_H
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
# include "globalshortcutbackend-gsd.h"
|
||||
# include "globalshortcutbackend-kde.h"
|
||||
#endif
|
||||
#if defined(HAVE_X11) || defined(Q_OS_WIN)
|
||||
# include "globalshortcutbackend-system.h"
|
||||
@@ -51,9 +52,11 @@
|
||||
|
||||
GlobalShortcuts::GlobalShortcuts(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
dbus_backend_(nullptr),
|
||||
gsd_backend_(nullptr),
|
||||
kde_backend_(nullptr),
|
||||
system_backend_(nullptr),
|
||||
use_gsd_(true),
|
||||
use_kde_(true),
|
||||
use_x11_(false)
|
||||
{
|
||||
|
||||
@@ -82,7 +85,8 @@ GlobalShortcuts::GlobalShortcuts(QWidget *parent)
|
||||
|
||||
// Create backends - these do the actual shortcut registration
|
||||
#ifdef HAVE_DBUS
|
||||
dbus_backend_ = new GlobalShortcutBackendGSD(this);
|
||||
gsd_backend_ = new GlobalShortcutBackendGSD(this);
|
||||
kde_backend_ = new GlobalShortcutBackendKDE(this);
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
@@ -106,6 +110,7 @@ void GlobalShortcuts::ReloadSettings() {
|
||||
|
||||
// The actual shortcuts have been set in our actions for us by the config dialog already - we just need to reread the gnome settings.
|
||||
use_gsd_ = settings_.value("use_gsd", true).toBool();
|
||||
use_kde_ = settings_.value("use_kde", true).toBool();
|
||||
use_x11_ = settings_.value("use_x11", false).toBool();
|
||||
|
||||
Unregister();
|
||||
@@ -149,6 +154,16 @@ bool GlobalShortcuts::IsGsdAvailable() const {
|
||||
|
||||
}
|
||||
|
||||
bool GlobalShortcuts::IsKdeAvailable() const {
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
return QDBusConnection::sessionBus().interface()->isServiceRegistered(GlobalShortcutBackendKDE::kKdeService);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
bool GlobalShortcuts::IsX11Available() const {
|
||||
|
||||
#ifdef HAVE_X11
|
||||
@@ -160,7 +175,9 @@ bool GlobalShortcuts::IsX11Available() const {
|
||||
}
|
||||
|
||||
void GlobalShortcuts::Register() {
|
||||
if (use_gsd_ && dbus_backend_ && dbus_backend_->Register()) return;
|
||||
|
||||
if (use_gsd_ && gsd_backend_ && gsd_backend_->Register()) return;
|
||||
if (use_kde_ && kde_backend_ && kde_backend_->Register()) return;
|
||||
#ifdef HAVE_X11 // If this system has X11, only use the system backend if X11 is enabled in the global shortcut settings
|
||||
if (use_x11_) {
|
||||
#endif
|
||||
@@ -169,11 +186,15 @@ void GlobalShortcuts::Register() {
|
||||
#ifdef HAVE_X11
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void GlobalShortcuts::Unregister() {
|
||||
if (dbus_backend_ && dbus_backend_->is_active()) dbus_backend_->Unregister();
|
||||
|
||||
if (gsd_backend_ && gsd_backend_->is_active()) gsd_backend_->Unregister();
|
||||
if (kde_backend_ && kde_backend_->is_active()) kde_backend_->Unregister();
|
||||
if (system_backend_ && system_backend_->is_active()) system_backend_->Unregister();
|
||||
|
||||
}
|
||||
|
||||
bool GlobalShortcuts::IsMacAccessibilityEnabled() const {
|
||||
|
||||