Compare commits

..

146 Commits
0.8.2 ... 0.8.4

Author SHA1 Message Date
Jonas Kvinge
fc07919a75 Release 0.8.4 2020-11-15 00:31:32 +01:00
Jonas Kvinge
d7661edf67 Update Changelog 2020-11-15 00:20:16 +01:00
Jonas Kvinge
1c91693294 Ignore org.kde.kglobalaccel.NoSuchComponent error 2020-11-15 00:20:08 +01:00
Jonas Kvinge
3fc3cbc6d5 Update protobuf dll in nsi 2020-11-14 19:13:40 +01:00
Jonas Kvinge
11b5895e69 Update ccpp.yml 2020-11-14 18:36:59 +01:00
Jonas Kvinge
b4c614edbf Set volume bit 2020-11-14 04:36:38 +01:00
Jonas Kvinge
deddaed04a Remove use of std::bind where possible 2020-11-14 02:13:22 +01:00
Jonas Kvinge
a155e503f4 Never use reference when iterating QJsonArray 2020-11-13 21:10:50 +01:00
Jonas Kvinge
c0663bc19f Use reference 2020-11-13 20:34:29 +01:00
Strawbs Bot
7ffa51b83d Update translations 2020-11-13 01:02:53 +01:00
Jonas Kvinge
6d397b9988 Fix smart playlist by filename 2020-11-12 20:47:13 +01:00
Jonas Kvinge
571a7fa26b Fix single letter collection nodes showing before dividers 2020-11-12 20:30:58 +01:00
Jonas Kvinge
b3b5a38c3a Minor code style fix 2020-11-11 22:55:56 +01:00
Jonas Kvinge
a4b115f89b Update Changelog 2020-11-11 22:51:55 +01:00
Strawbs Bot
3d672bb145 Update translations 2020-11-11 01:03:35 +01:00
Jonas Kvinge
15b656b753 Merge pull request #587 from fbugno/issue515
Fix HiDPI scaling for glow animation and drag over playlist
2020-11-10 22:52:08 +01:00
Felipe Bugno
f5785db163 Code style changes to match the existing code
This changes the style of the private variable and the call
convention of the inherited functions.
2020-11-10 17:55:00 -03:00
Jonas Kvinge
1ff1bf3292 Only call deleteLater when proxystyle is set 2020-11-10 19:06:26 +01:00
Felipe Bugno
b062febea0 Fix HiDPI scaling for glow animation and drag over playlist
This set the proper scaling and pixel ratio of QPixmap widgets
used as cached objects.

Most of cached objects uses a custom QPaint instead of the default
painter object from the parent widget. The problem is that, unlike
the painter from the parent object, set by the main application,
and that has DPI and scaling settings from the device, these custom
QPainters don't know about the underlying device, thus uses a
scale of 1 to render artifacts.

When a cached object "edited" by a custom QPaint along his pipeline
where used on a paint or drawrow routine, his stored image is distorted
and burred in a effort to resize it to the display configuration.
2020-11-09 21:49:22 -03:00
Strawbs Bot
35301dc79e Update translations 2020-11-10 01:02:36 +01:00
Jonas Kvinge
30c336726b Only backup database if schema version is correct 2020-11-09 23:10:43 +01:00
Jonas Kvinge
3821680817 Increase sqlite busy timeout to 30 seconds
Possible fix for #533
2020-11-09 22:49:33 +01:00
Jonas Kvinge
74242ea24f Fix shortcut settings on macOS and Windows 2020-11-09 20:20:31 +01:00
Jonas Kvinge
73c7024e11 Dont return from SongSaveComplete early, needs to free TagReaderReply 2020-11-09 19:17:31 +01:00
Strawbs Bot
bbdec92dc6 Update translations 2020-11-09 01:01:55 +01:00
Jonas Kvinge
b4c289101c Strip summary but encode message 2020-11-08 21:54:16 +01:00
Jonas Kvinge
722d0797f6 Use QString::toHtmlEscaped() 2020-11-08 17:52:29 +01:00
Jonas Kvinge
deb27d5b55 Replace '&' with '&' in D-BUS OSD messages 2020-11-08 14:24:09 +01:00
Jonas Kvinge
9cc4ffdf6e Only strip '&' from D-Bus OSD messages 2020-11-08 13:46:17 +01:00
Strawbs Bot
c76d63d1d9 Update translations 2020-11-08 13:32:51 +01:00
Jonas Kvinge
21bb4f33ad Update Changelog 2020-11-08 04:05:39 +01:00
Jonas Kvinge
2897b881d6 Only override fancy tabwidget style with adwaita 2020-11-08 04:04:37 +01:00
Jonas Kvinge
e9b89d0929 Simplify FancyTabWidget override 2020-11-08 03:23:18 +01:00
Jonas Kvinge
e801254b2e Log used style 2020-11-08 03:22:38 +01:00
Jonas Kvinge
6f49918ee9 Update Changelog 2020-11-08 02:10:20 +01:00
Jonas Kvinge
0ae7c18f1f Add setting to set style 2020-11-08 02:04:24 +01:00
Jonas Kvinge
c9c3fb396a Move ComboBoxLoadFromSettings function to settingspage 2020-11-08 02:02:48 +01:00
Strawbs Bot
91db4f1934 Update translations 2020-11-08 01:02:31 +01:00
Jonas Kvinge
9bbed6e95c Use QClipboard::setText instead of QClipboard::setMimeData
Fixes #581

Fixes clipboard copying on Windows too
2020-11-08 00:51:24 +01:00
Strawbs Bot
748bc27b25 Update translations 2020-11-06 11:50:48 +01:00
Jonas Kvinge
f42708e8bc Remove qt6-qt5compat-devel from ccpp.yml 2020-11-05 22:30:37 +01:00
Jonas Kvinge
160e4570a2 Use C++17 (#579)
* Use C++17

* Replace std::random_shuffle with std::shuffle

* Add random include
2020-11-05 22:28:49 +01:00
Jonas Kvinge
6272965143 Update Changelog 2020-11-05 00:10:16 +01:00
Jonas Kvinge
1e9613bf7f Update ccpp.yml 2020-11-04 23:45:40 +01:00
Jonas Kvinge
a061dac298 Check network proxy mode 2020-11-04 22:22:26 +01:00
Jonas Kvinge
914dee8571 Pass network proxy settings to gstreamer
Fixes #558
2020-11-04 22:16:20 +01:00
yavuzmert
47e2905edf Re-enable progress on system tray icon (#578)
* Re-enable progress on system tray icon

* fix copy-paste typo in mac sources

* Make tray icon progress optional

* Move tray icon progress setting control to MainWindow

* Move trayicon settings to behavioursettings

Co-authored-by: Yavuz Mert <yavuz.mert@darkbluesystems.net>
2020-11-04 21:01:04 +01:00
Jonas Kvinge
ee6675aee0 No longer need Core5Compat 2020-11-04 18:17:33 +01:00
Jonas Kvinge
62e0d9fe64 Remove all uses of QTextCodec 2020-11-04 18:16:23 +01:00
Jonas Kvinge
a174c142c1 Remove unused linked list includes 2020-11-04 18:06:36 +01:00
Jonas Kvinge
95afc5fdec Keep tabs in the middle on macOS 2020-11-04 18:05:58 +01:00
Strawbs Bot
04d69f66c0 Update translations 2020-11-04 01:02:16 +01:00
Jonas Kvinge
0347141edd Update snap dialog 2020-11-03 19:32:32 +01:00
Jonas Kvinge
1d6baae6e0 Set ssl-strict false in songloader too 2020-11-03 18:48:35 +01:00
yavuzmert
fb0f48f08a Make context view top label selectable (#576)
Co-authored-by: Yavuz Mert <yavuz.mert@darkbluesystems.net>
2020-11-03 01:34:09 +01:00
Jonas Kvinge
76e5e03d31 Change copy command in snap dialog 2020-11-02 21:26:20 +01:00
Jonas Kvinge
4cab743634 Center playlist tabbar star icon
Fixes #574
2020-11-02 17:57:12 +01:00
Jonas Kvinge
7c10ec97b7 Remove unused parameter 2020-11-02 17:47:16 +01:00
Jonas Kvinge
8718a16889 Star/unstar playlist with doubleclick 2020-11-02 17:45:29 +01:00
Strawbs Bot
75a0b924c3 Update translations 2020-11-02 01:02:00 +01:00
Jonas Kvinge
83a90e0c05 Make tabbar style hack less intrusive 2020-11-01 21:54:23 +01:00
Strawbs Bot
e5eadd1315 Update translations 2020-10-31 14:34:41 +01:00
Jonas Kvinge
f0142d90d4 Update comment 2020-10-31 14:27:53 +01:00
Jonas Kvinge
cabd6e6e9d Override QStyle::subElementRect in fancy tabbar to fix style problems
Something is causing the contents of the tabbar to be stretched from top to bottom with space between icons and text.
You can see this on the default Fedora (Gnome) installation.
Also fixes the tabbar on macOS where the content was in the middle instead of the top.
2020-10-31 14:05:06 +01:00
Jonas Kvinge
4804a05736 Remove fixed width in about dialog as it crashes on wayland 2020-10-31 02:41:42 +01:00
Jonas Kvinge
c258e5a3af Add KDE global shortcuts
Fixes #572
2020-10-31 02:08:19 +01:00
Jonas Kvinge
e8492940a5 Add -fPIC compiler flag for Redhat/Fedora/CentOS
Fixes #573
2020-10-30 18:12:42 +01:00
Jonas Kvinge
4bccb1ab47 Only save as ID3v2, and remove empty tags
Fixes #571
2020-10-29 18:47:09 +01:00
Strawbs Bot
b6d219e232 Update translations 2020-10-28 17:50:58 +01:00
Jonas Kvinge
23ee17594d Simplify CMake Qt 2020-10-27 20:54:25 +01:00
Jonas Kvinge
d9d39d8e25 Only override MainWindow::nativeEvent for Windows 2020-10-27 20:17:28 +01:00
Jonas Kvinge
224d5d46c1 Set QApplication::setQuitOnLastWindowClosed to false 2020-10-27 20:01:07 +01:00
Jonas Kvinge
09e0059930 Resize organize window when copying to device
Fixes #566
2020-10-27 17:50:16 +01:00
Jonas Kvinge
0ddff2b087 Fix stupid bug 2020-10-27 17:47:40 +01:00
Jonas Kvinge
2e6a29eacc Remove ifdef HAVE_GSTREAMER
Fixes #568
2020-10-27 17:17:12 +01:00
Jonas Kvinge
ad2fb82aa9 Don't edit playlist name on doubleclick in playlists view
Fixes #567
2020-10-27 17:11:17 +01:00
Strawbs Bot
a3f91c11e8 Update translations 2020-10-26 01:02:24 +01:00
Jonas Kvinge
a50c978ce3 Fix cancelling logout when window is maxmimized
Fixes #565
2020-10-25 19:59:27 +01:00
Strawbs Bot
27d6f881cd Update translations 2020-10-25 01:04:50 +02:00
Jonas Kvinge
944cd020af Only strip problematic characters when saving a playlist 2020-10-25 01:01:43 +02:00
Jonas Kvinge
bbe5d64b99 Add info about backing up configuration to snap dialog 2020-10-25 01:01:16 +02:00
Jonas Kvinge
f7b36ac4c7 Replace use of QVariant::type() with Qt 6 2020-10-24 03:32:40 +02:00
Jonas Kvinge
1d555ca17e Turn back git revision 2020-10-24 03:30:21 +02:00
Jonas Kvinge
f91b6c3468 Release 0.8.3 2020-10-24 01:32:25 +02:00
Jonas Kvinge
abe6eeb350 Update Changelog 2020-10-23 19:35:07 +02:00
Jonas Kvinge
64f90a7912 Remove Qt MacExtras, no longer used 2020-10-22 23:11:44 +02:00
Jonas Kvinge
5733966843 Dont reset settingspage changed status when window is shown
This is done by Init in Load() when the settings is opened.
2020-10-22 20:25:31 +02:00
Jonas Kvinge
eb1344fcec Unref caps in HandoffCallback 2020-10-22 17:49:13 +02:00
Strawbs Bot
c5fb29f00e Update translations 2020-10-22 01:02:03 +02:00
Jonas Kvinge
63135b9c54 Engine will never be in playing state on error 2020-10-21 23:27:15 +02:00
Jonas Kvinge
f7c666584e Remove debug line 2020-10-21 21:33:26 +02:00
Jonas Kvinge
6834324de2 Add more unit tests for organizeformat 2020-10-21 21:32:30 +02:00
Jonas Kvinge
3a0d59e66f Only append path if not empty 2020-10-21 21:32:12 +02:00
Jonas Kvinge
ffd2e2188a Fix crash with empty APE tag for MPC files 2020-10-21 20:29:06 +02:00
Jonas Kvinge
0e8d5bdc5d Fix crash with empty APE tag for WavPack and APE files 2020-10-21 20:27:43 +02:00
Jonas Kvinge
14806f6614 Update Changelog 2020-10-21 19:57:00 +02:00
Jonas Kvinge
00ece83b9d Tidal: Set default quality to lossless 2020-10-21 19:56:37 +02:00
Jonas Kvinge
8197ae2a2d Tidal: Guess filetype by filename extension in URL when missing codec. 2020-10-21 01:12:46 +02:00
Strawbs Bot
5fe658bb16 Update translations 2020-10-21 01:02:16 +02:00
Jonas Kvinge
617179f0c6 Always set state to NULL in destructor 2020-10-21 00:32:55 +02:00
Jonas Kvinge
95ac85f642 Move stream discoverer from pipeline to engine
Fixes #491
2020-10-21 00:07:58 +02:00
Jonas Kvinge
ca8877ad47 Revert gst_discoverer_stop 2020-10-20 18:47:40 +02:00
Jonas Kvinge
6d8f31048c Add call to gst_discoverer_stop
Stream discoverer currently only works on Linux
2020-10-20 18:28:09 +02:00
Jonas Kvinge
ac859eb576 Make sure we query empty instead of null 2020-10-20 17:14:38 +02:00
Jonas Kvinge
2dfa171b6a Save effective_albumartist as empty istead of null
Since we use the metadata now instead of the container keys for queries in
the model, there is a regression when the values are null.
2020-10-20 17:12:28 +02:00
Jonas Kvinge
6f72e3e2ea Always append divider key to sort text if the key is a digit 2020-10-20 17:11:14 +02:00
Strawbs Bot
c2b73ae963 Update translations 2020-10-20 01:04:50 +02:00
Jonas Kvinge
6c50077409 Fix concurrentrun test 2020-10-19 22:33:08 +02:00
Jonas Kvinge
06746449a1 Disable TestContainerNodes 2020-10-19 22:32:50 +02:00
Jonas Kvinge
da7b8edf51 Revert back to using PathWithoutFilenameExtension in organizedialog 2020-10-19 21:35:39 +02:00
Jonas Kvinge
6e29b41f23 Update Changelog 2020-10-19 21:07:25 +02:00
Jonas Kvinge
912bb069af Fix updating album cover to collection for edit tag dialog 2020-10-19 21:05:59 +02:00
Jonas Kvinge
6b2d7a67d8 Use Utilities::FiddleFileExtension in organize 2020-10-19 19:56:40 +02:00
Jonas Kvinge
dbb8ec0290 Remove debug in bus callbacks 2020-10-19 19:09:48 +02:00
Strawbs Bot
60b32760f2 Update translations 2020-10-19 01:03:27 +02:00
Jonas Kvinge
73a40bcb49 Update Changelog 2020-10-18 17:08:28 +02:00
Jonas Kvinge
0e258a5a32 Remove unused utilities functions 2020-10-18 14:12:01 +02:00
Jonas Kvinge
7ca65c81d8 Use QFileInfo instead of custom functions 2020-10-18 14:07:48 +02:00
Jonas Kvinge
2ad1a60e59 Use correct file extension in organize preview and resulting filename
Fixes #564
2020-10-18 13:24:33 +02:00
Jonas Kvinge
cf17ff4478 Update Changelog 2020-10-18 00:29:28 +02:00
Jonas Kvinge
fffc3aac68 Update Changelog 2020-10-17 21:11:06 +02:00
Jonas Kvinge
295ac3c458 Add back thumbbar 2020-10-17 21:10:57 +02:00
Jonas Kvinge
b6693a71f9 Rename initialise to initialize 2020-10-17 17:29:09 +02:00
Jonas Kvinge
5b21118a8c Replace gst_tag_list_free with gst_tag_list_unref 2020-10-17 04:54:46 +02:00
Jonas Kvinge
0235b19801 Only update buttons once 2020-10-17 04:21:11 +02:00
Jonas Kvinge
7426399aa2 Enable thumbbar for debug build 2020-10-17 03:41:28 +02:00
Jonas Kvinge
6861b0d668 Possible fix Windows thumbbar 2020-10-17 03:37:39 +02:00
Jonas Kvinge
c30fb0d38c Log errors in MM device finder 2020-10-17 03:19:13 +02:00
Jonas Kvinge
e45521c6c0 Fix updating playing widget song details in small mode 2020-10-16 23:57:18 +02:00
Strawbs Bot
d11fe8d4fc Update translations 2020-10-16 01:01:51 +02:00
Jonas Kvinge
8e83e63e3d Add snap warning dialog 2020-10-15 21:47:52 +02:00
Jonas Kvinge
c8fd0ac4b3 Update snapcraft.yaml 2020-10-15 18:51:21 +02:00
Jonas Kvinge
d78419eb33 Enable WASAPI plugin
Fixes #283
2020-10-15 16:08:59 +02:00
Jonas Kvinge
e44a3d013d Disable windows thumbbar
Fixes stability issues with WASAPI
2020-10-15 16:07:20 +02:00
Jonas Kvinge
aeb0d05017 Add CoInitializeEx() to mmdevice finder and refactor code 2020-10-15 16:06:07 +02:00
Jonas Kvinge
62702e4b3d Add space 2020-10-14 22:53:58 +02:00
Jonas Kvinge
5146cdfa2f Improve windows thumbbar code 2020-10-14 22:53:08 +02:00
Jonas Kvinge
246e7018c3 Remove concurrentrun.h 2020-10-14 22:50:19 +02:00
Jonas Kvinge
24286dbe9d Use QtConcurrent::run directly 2020-10-14 22:49:37 +02:00
Jonas Kvinge
2f442dfbe1 Ignore return value 2020-10-14 22:38:32 +02:00
Jonas Kvinge
b2fb01ee9c Use QtConcurrent 2020-10-14 22:35:54 +02:00
Jonas Kvinge
45e0a9a4ef Remove build-snap from CI 2020-10-14 18:36:29 +02:00
Jonas Kvinge
675b7b4bf4 Fix CI 2020-10-14 18:05:04 +02:00
Jonas Kvinge
be7a35443e Add gdb to nsi 2020-10-14 16:55:17 +02:00
Strawbs Bot
9918615fcd Update translations 2020-10-14 01:02:02 +02:00
Jonas Kvinge
4b72ef77c1 Turn back git revision 2020-10-14 00:26:35 +02:00
178 changed files with 6531 additions and 4949 deletions

View File

@@ -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 }}

View File

@@ -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}

View File

@@ -1,2 +1 @@
cmake_minimum_required(VERSION 3.0)
set(CMAKE_CXX_STANDARD 11)

View File

@@ -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>

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)

View File

@@ -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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

1
dist/CMakeLists.txt vendored
View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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})

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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(); });
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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];

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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());
}

View File

@@ -58,6 +58,7 @@ protected:
private:
QPixmap normal_icon_;
QPixmap grey_icon_;
std::unique_ptr<MacSystemTrayIconPrivate> p_;
Q_DISABLE_COPY(MacSystemTrayIcon);
};

View File

@@ -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_);
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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());

View File

@@ -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());

View File

@@ -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();

View File

@@ -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_);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View 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>

View 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&lt;int&gt;"/>
</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&lt;QStringList&gt;"/>
</method>
<method name="allActionsForComponent">
<arg type="aas" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList&lt;QStringList&gt;"/>
<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&lt;int&gt;"/>
<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&lt;int&gt;"/>
<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&lt;int&gt;"/>
<arg name="actionId" type="as" direction="in"/>
<arg name="keys" type="ai" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QList&lt;int&gt;"/>
<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&lt;int&gt;"/>
</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>

View File

@@ -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

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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_;

View File

@@ -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>

View File

@@ -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
View 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
View 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
View 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>

View File

@@ -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) {

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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:

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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";
}
}

View File

@@ -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 */

View File

@@ -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";
}
}

View File

@@ -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_;

View File

@@ -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();

View File

@@ -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_;
};

View File

@@ -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();
};

View File

@@ -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;

View File

@@ -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();
};

View File

@@ -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_) {

View File

@@ -36,7 +36,7 @@ class PulseDeviceFinder : public DeviceFinder {
explicit PulseDeviceFinder();
~PulseDeviceFinder() override;
bool Initialise() override;
bool Initialize() override;
QList<Device> ListDevices() override;
private:

View File

@@ -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;

View File

@@ -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);

View 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();
}
}
}

View 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

View File

@@ -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 {

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