Compare commits

..

397 Commits
0.7.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
Jonas Kvinge
e427c61fbb Release 0.8.2 2020-10-13 18:03:05 +02:00
Jonas Kvinge
c78e8937d5 Update ISSUE_TEMPLATE.md 2020-10-13 01:50:32 +02:00
Jonas Kvinge
5d63f7a93d Update ISSUE_TEMPLATE.md 2020-10-13 01:50:03 +02:00
Jonas Kvinge
46551ada7f Update Changelog 2020-10-13 01:47:22 +02:00
Jonas Kvinge
83f17a37b1 Use format only when available 2020-10-13 01:38:09 +02:00
Strawbs Bot
9e23e0a623 Update translations 2020-10-13 01:05:54 +02:00
Jonas Kvinge
4a53d4f043 Ignore "IDirectSoundBuffer_GetStatus The operation completed successfully"
Fixes #557
2020-10-13 00:49:34 +02:00
Jonas Kvinge
0fd61945c7 Fix saving initial settings 2020-10-12 17:20:18 +02:00
Strawbs Bot
e3624eed30 Update translations 2020-10-12 01:03:47 +02:00
Jonas Kvinge
1923c8be0c Remove unused includes 2020-10-11 01:31:12 +02:00
Jonas Kvinge
a3e37fbfe2 Remove use of HTML in system tray icon tooltip 2020-10-11 01:29:26 +02:00
Jonas Kvinge
318c3bb422 Check if QNetworkRequest::ContentTypeHeader is filetype everwhere 2020-10-11 01:08:42 +02:00
Strawbs Bot
db96e24028 Update translations 2020-10-11 01:02:04 +02:00
Jonas Kvinge
cc2cce66df Make const 2020-10-11 00:07:38 +02:00
Jonas Kvinge
7afeabd288 Subsonic: Read created from album too
Fixes #526
2020-10-11 00:05:06 +02:00
Jonas Kvinge
eb43a812d6 Fix saving subsonic cover when ContentTypeHeader returns filetype 2020-10-10 23:46:29 +02:00
Jonas Kvinge
12cbcdb6f4 Fix SQL query by song id when song id is a string 2020-10-10 23:44:42 +02:00
Jonas Kvinge
a2e35e30dc Use lowercase for divider keys sort text
Fixes #556
2020-10-10 18:44:57 +02:00
Jonas Kvinge
b6ff7e6b47 Fix transition to next song in CUE files
Fixes #552
2020-10-10 01:57:02 +02:00
Jonas Kvinge
6dba40c6bb Turn back git revision 2020-10-10 01:21:13 +02:00
Jonas Kvinge
896da46422 Release 0.8.1 2020-10-09 21:38:00 +02:00
Jonas Kvinge
7c07f5eb2a Add clang-format file 2020-10-09 21:02:18 +02:00
Strawbs Bot
298cff37de Update translations 2020-10-09 01:01:57 +02:00
Jonas Kvinge
8add802fe9 Update Changelog 2020-10-08 19:42:33 +02:00
Jonas Kvinge
6d080a0d59 Fix crash when copying a closed playlist to a device
Fixes #551
2020-10-08 19:19:39 +02:00
Jonas Kvinge
f0ae1051ee Only set art manual for temporary metadata when temp metadata is set 2020-10-08 19:14:56 +02:00
Strawbs Bot
1ad0ffeaa6 Update translations 2020-10-08 01:02:36 +02:00
Jonas Kvinge
45b44d012d Fix source for CUE songs in collection watcher 2020-10-07 23:27:58 +02:00
Jonas Kvinge
cb6cbb9ee5 Remove unused screensaver classes 2020-10-07 22:10:31 +02:00
Jonas Kvinge
013a0d9cb0 Remove commented line 2020-10-07 21:28:52 +02:00
Jonas Kvinge
8abb3ea225 Fix backend settings tab order 2020-10-07 20:43:15 +02:00
Jonas Kvinge
74a5233b5d Replace use of deprecated gstreamer low-percent
- Add settings for low-watermark and high-watermark
- Add button to reset buffer settings to defaults
2020-10-07 20:29:26 +02:00
Strawbs Bot
cd695a4522 Update translations 2020-10-06 01:02:00 +02:00
Jonas Kvinge
eec4b2cc0e Call QLocalSocket::flush() after QLocalSocket::waitForBytesWritten() 2020-10-05 22:05:37 +02:00
Jonas Kvinge
616875d0d2 Log when sending message to primary instance fails 2020-10-05 22:04:58 +02:00
Jonas Kvinge
eb749bd76f Fix compile 2020-10-05 21:40:31 +02:00
Jonas Kvinge
7d1e404efd Fix header guard 2020-10-05 21:34:45 +02:00
Jonas Kvinge
4a01a578d1 Update singleapplication 2020-10-05 21:31:03 +02:00
Jonas Kvinge
393a2e0ea0 Set hide to false when window is shown 2020-10-05 19:08:09 +02:00
Jonas Kvinge
8e21decb8d Add WASAPI plugin to own setup file
Seem to still be stability issues.
2020-10-04 19:24:08 +02:00
Jonas Kvinge
22bfbab832 Add check for desktop notifications service version 2020-10-04 04:38:46 +02:00
Jonas Kvinge
28d0cc8795 Format singleapplication sources 2020-10-04 02:18:10 +02:00
Jonas Kvinge
51f2383a07 Remove lyrics from fandom.com
API no longer exists.
2020-10-04 02:13:06 +02:00
Jonas Kvinge
a6426c6eba Update Changelog 2020-10-04 02:00:12 +02:00
Jonas Kvinge
0631da6c8e Register Tidal URL Scheme in Windows installer 2020-10-04 01:59:40 +02:00
Jonas Kvinge
3960239cfe Add back WASAPI plugin again 2020-10-03 20:30:14 +02:00
Jonas Kvinge
b16e48af6f Add back WASAPI plugin again 2020-10-03 20:29:05 +02:00
Jonas Kvinge
4d3950565a Remove scangiomodulepath.cpp from CMakeLists.txt 2020-10-03 20:10:38 +02:00
Jonas Kvinge
8d24bc50c9 Change GST to GStreamer in debug logging 2020-10-03 20:08:33 +02:00
Jonas Kvinge
458efe9168 Remove gstreamer registry workaround for Windows 2020-10-03 20:05:30 +02:00
Jonas Kvinge
ae940a8681 Fix PRODUCT_NAME 2020-10-03 19:37:29 +02:00
Jonas Kvinge
d76b223b7c Fix typo 2020-10-03 19:36:10 +02:00
Jonas Kvinge
7243d5f7cb Add WASAPI plugin to own setup 2020-10-03 19:29:04 +02:00
Jonas Kvinge
387f74f66a Revert "Add back WASAPI plugin"
This reverts commit a553693f34.
2020-10-03 17:38:31 +02:00
Jonas Kvinge
61ffb7d97a Unref bus 2020-10-03 13:09:09 +02:00
Jonas Kvinge
a553693f34 Add back WASAPI plugin
Fixes #283
2020-10-03 02:03:37 +02:00
Jonas Kvinge
cdcfd64ec4 Possible fix for WASAPI crashes 2020-10-03 01:58:52 +02:00
Jonas Kvinge
7f4302ba20 Update Changelog 2020-10-02 22:09:40 +02:00
Jonas Kvinge
0d94f2d376 Update README 2020-10-02 22:09:33 +02:00
Jonas Kvinge
c1eb20a20b Force inform in Player::NextItem 2020-10-02 20:38:14 +02:00
Jonas Kvinge
634db67685 Change branch to master 2020-10-02 19:29:55 +02:00
Jonas Kvinge
86648258a3 Add CI for snap 2020-10-02 19:29:26 +02:00
Jonas Kvinge
287f0a3739 Remove redundant check for isNull() 2020-10-02 19:27:47 +02:00
Strawbs Bot
8f42df209a Update translations 2020-10-02 01:07:32 +02:00
Jonas Kvinge
390fd64a74 Save initial settings 2020-10-01 22:04:38 +02:00
Jonas Kvinge
48ee471def Remove force inform in PreviousItem 2020-10-01 20:30:31 +02:00
Jonas Kvinge
e862afcb78 Log song change 2020-10-01 20:09:36 +02:00
Jonas Kvinge
872da05ff6 Inform of song change on play restart, add playlist auto sorting.
Fixes #511
2020-10-01 19:58:16 +02:00
Jonas Kvinge
d09e2daf00 Mark backend settings changed if loaded engine, output or device does
not match the configured.
2020-10-01 19:53:07 +02:00
Jonas Kvinge
e2d5b44b0a Hide certain playlist list context menu options when items are not selected 2020-10-01 19:49:06 +02:00
Jonas Kvinge
52d42ef2a8 Use BUILD_WITH_QT6 2020-10-01 19:47:21 +02:00
Jonas Kvinge
b6abc34461 Set CollectionWatcher::sValidImages directly 2020-10-01 19:45:10 +02:00
Jonas Kvinge
ae6a50626d Change Qt 5/6 option 2020-10-01 19:43:39 +02:00
Jonas Kvinge
8f9dbfee2c Replace QMacCocoaViewContainer with QWidget::createWindowContainer 2020-10-01 19:40:55 +02:00
Jonas Kvinge
c71ccda967 Remove '&' in OSD messages
Causes weird problems with previous text getting stuck.
2020-10-01 19:39:49 +02:00
Strawbs Bot
0f284f2e1d Update translations 2020-10-01 01:01:35 +02:00
Jonas Kvinge
45ac80dd8c Change order of artist - title when song is paused/stopped in OSD 2020-09-30 01:14:00 +02:00
Jonas Kvinge
09cdba4b3d Fix minor code issues 2020-09-30 01:02:41 +02:00
Jonas Kvinge
d94ee8863c Fix playing widget stuck on error 2020-09-30 00:44:40 +02:00
Jonas Kvinge
cc8ced6430 Disable live scanning option 2020-09-30 00:03:06 +02:00
Jonas Kvinge
cdb144e6a6 Update Changelog 2020-09-29 23:26:55 +02:00
Jonas Kvinge
58fb8643c6 Fix typo in README 2020-09-29 23:26:26 +02:00
Jonas Kvinge
b81cfa8acb Update stream url to avoid requesting it twice 2020-09-29 23:03:50 +02:00
Jonas Kvinge
938ee20f1f Make sure song changed is only called once 2020-09-29 22:40:43 +02:00
Jonas Kvinge
d02dc54c1b Log remote cover loading 2020-09-29 22:30:44 +02:00
Jonas Kvinge
8680a54ae4 Only draw text when show/hide timeline is finished 2020-09-29 20:22:11 +02:00
Jonas Kvinge
30001be0ee Show song in OSD when pausing and stopping 2020-09-29 19:27:35 +02:00
Jonas Kvinge
4614cb5ec1 Make sure same cover isn't loaded twice 2020-09-29 17:51:31 +02:00
Jonas Kvinge
e390f3a399 Clear now playing in MainWindow::MediaPlaying() instead
Fixes #548
2020-09-29 17:38:00 +02:00
Jonas Kvinge
e22d463d11 Use QFileInfo::completeBaseName() instead of QFileInfo::baseName()
Fixes #550
2020-09-29 17:30:21 +02:00
Jonas Kvinge
5877aa822c Don't reset playing widget timeline 2020-09-29 01:07:04 +02:00
Strawbs Bot
a2915913bb Update translations 2020-09-29 01:02:40 +02:00
Jonas Kvinge
a8b40747b2 Attempt to improve playing widget up/down 2020-09-28 20:09:23 +02:00
Jonas Kvinge
b63030d302 Don't send now playing twice
Fixes #548
2020-09-28 17:47:45 +02:00
Strawbs Bot
4457e416db Update translations 2020-09-26 01:01:50 +02:00
Pascal Below
c7c1a8ede1 SubsonicRequest: create cover directory if it doesn't exist (#547) 2020-09-25 15:46:25 +02:00
Jonas Kvinge
fd1e9d7fb0 Fix clearing sent in scrobbler cache 2020-09-24 17:02:43 +02:00
Strawbs Bot
59a6d2317b Update translations 2020-09-24 01:02:04 +02:00
Jonas Kvinge
531c171542 Merge branch 'debian' into master 2020-09-23 19:06:22 +02:00
Jonas Kvinge
0c743452b0 Only compile Subsonic scrobbler when compiled with Subsonic support 2020-09-23 18:55:22 +02:00
Jonas Kvinge
4893d3da1f Add back debian bullseye and add dh-make to ubuntu too 2020-09-23 18:42:56 +02:00
Jonas Kvinge
5cdc24bfb0 Reload Subsonic scrobbler settings 2020-09-23 18:31:09 +02:00
Jonas Kvinge
523cdca449 Add back dh-make 2020-09-23 18:23:59 +02:00
Jonas Kvinge
09537d73da Remove debian bullseye CI 2020-09-23 18:10:13 +02:00
Jonas Kvinge
99c0c0b3b1 Update Changelog 2020-09-23 17:59:51 +02:00
Pascal Below
45bc353341 Add Subsonic scrobble support (#545)
* add SubsonicScrobbler, add Scrobble method in SubsonicService

* new class SubsonicScrobbleRequest, use queue again, clean up

* add checkbox to enable server-side scrobbling to Subsonic settings page

* Check serversidescrobbling in SubsonicScrobbler::ReloadSettings instead of SubsonicService

TODO: SubsonicScrobbler::ReloadSettings needs to be called when
SubsonicSettings change.
2020-09-23 17:55:12 +02:00
Strawbs Bot
b2fc41a911 Update translations 2020-09-23 01:02:50 +02:00
Jonas Kvinge
ebefe8b6d2 Update copyright 2020-09-23 00:55:34 +02:00
Jonas Kvinge
9e3508134b Add compilation to edit tag dialog 2020-09-23 00:52:41 +02:00
Jonas Kvinge
3166ca2127 Use QRecursiveMutex 2020-09-22 18:58:44 +02:00
Jonas Kvinge
204508478f Don't ignore closeevent if already hidden
Possible fix for #277
2020-09-22 18:40:37 +02:00
Strawbs Bot
51ce674c97 Update translations 2020-09-22 01:02:29 +02:00
Jonas Kvinge
6fcde9fe5f Add qobuz to scrobbler settings 2020-09-22 00:00:02 +02:00
Jonas Kvinge
c688e3431d Fix debian builds 2020-09-20 15:55:25 +02:00
Jonas Kvinge
230376c7f3 Ignore unused variable in MoveToTrashRecursive 2020-09-20 14:55:52 +02:00
Jonas Kvinge
91d0a2cd0c Fix uninitialized variable 2020-09-19 02:10:16 +02:00
Jonas Kvinge
1ffd010e4a Add windows.h include to MM device finder
Fixes compile error with mingw-w64 8.0.0
2020-09-19 02:06:15 +02:00
Strawbs Bot
ae366e141f Update translations 2020-09-19 01:01:38 +02:00
Strawbs Bot
eb869c6e97 Update translations 2020-09-18 01:08:32 +02:00
Jonas Kvinge
b559f7cd16 Fix minor code issues 2020-09-17 18:12:14 +02:00
Jonas Kvinge
375d32f08e Add missing utilities include 2020-09-17 18:01:24 +02:00
Jonas Kvinge
89d6b7cec0 Add smart playlists, ratings and Qobuz
Fixes #259
Fixes #264
2020-09-17 17:50:17 +02:00
Jonas Kvinge
fdf96e8342 Update README 2020-09-17 17:49:39 +02:00
Jonas Kvinge
c5d73d7b09 Update Changelog 2020-09-17 17:49:25 +02:00
Jonas Kvinge
34b4cc2d9e Remove unused function 2020-09-17 17:28:32 +02:00
Jonas Kvinge
40ed17e609 Change defaults for context 2020-09-17 17:12:01 +02:00
Jonas Kvinge
673d76af45 Change defaults for Tidal 2020-09-17 17:07:54 +02:00
Jonas Kvinge
c13fb6f9d5 Use QLibraryInfo::path with Qt 6 2020-09-17 17:00:45 +02:00
Jonas Kvinge
424b1e7d1f Fix strawberry.spec for Mageia 2020-09-17 16:28:16 +02:00
Jonas Kvinge
e80c9c4101 Fix strawberry.spec for CentOS and Mageia 2020-09-16 19:21:37 +02:00
Jonas Kvinge
f75c3633e9 Add Fedora 34 CI 2020-09-16 18:11:34 +02:00
Jonas Kvinge
d535cd8276 Add Fedora 33 to CI 2020-09-16 18:08:33 +02:00
Jonas Kvinge
c13f6ab2af Remove brew unlink python@2 2020-09-16 18:05:49 +02:00
Jonas Kvinge
d2a30bfb78 Simplify strawberry.spec 2020-09-16 18:03:34 +02:00
Jonas Kvinge
e5b17092b4 Fix stretchheaderview column widths too wide 2020-09-16 00:01:16 +02:00
Strawbs Bot
466cb4c78b Update translations 2020-09-15 01:01:29 +02:00
Strawbs Bot
9722e1ddc4 Update translations 2020-09-13 01:01:42 +02:00
Jonas Kvinge
e3a9b0b943 Use collection settings for group by settings 2020-09-12 15:48:26 +02:00
Jonas Kvinge
d668a8aee2 Change sort text for divider keys
Fixes problems on Windows with specific regionale settings, where
divider keys are all on the top, while albums are on the bottom
2020-09-12 13:30:45 +02:00
Strawbs Bot
75dc0aafcf Update translations 2020-09-12 01:02:03 +02:00
Jonas Kvinge
41b94233c6 Add group by version check in internet search view too 2020-09-12 00:32:50 +02:00
Jonas Kvinge
52cff01b9c Fix group by version 2020-09-12 00:18:08 +02:00
Jonas Kvinge
372fc6603d Update .github/workflows/ccpp.yml 2020-09-11 23:35:21 +02:00
Jonas Kvinge
9221797c9d Add sqlite3.exe to nsi 2020-09-11 22:58:56 +02:00
Strawbs Bot
7501131558 Update translations 2020-09-11 01:04:26 +02:00
Jonas Kvinge
bba7be99a3 Remove override.h 2020-09-10 23:41:53 +02:00
Jonas Kvinge
abb95534d0 Add musepack to macdeploy 2020-09-10 23:06:10 +02:00
Jonas Kvinge
0b7b4c12e4 Add test for collection model container nodes 2020-09-10 22:13:20 +02:00
Jonas Kvinge
4056f00169 Only initialize translations in test when compiled with translations 2020-09-10 22:11:40 +02:00
Jonas Kvinge
10303cb9c0 Use unique keys for all container nodes in collection model
Fixes #539 and probably many more issues with the model.
2020-09-10 22:09:24 +02:00
Jonas Kvinge
e3587d369e Add const 2020-09-10 22:05:12 +02:00
Jonas Kvinge
2a048502cc Add PlaylistItem::NewFromSong function 2020-09-10 22:04:11 +02:00
Jonas Kvinge
cac01fbde9 Dont seperate compilation albums by directory in GetAlbums
Fixes issue where multi-disc albums are showing more than once in cover
manager.
2020-09-10 21:27:07 +02:00
Jonas Kvinge
2d2ce191ec Change gstreamer libraries in macdeploy 2020-09-10 19:15:06 +02:00
Jonas Kvinge
6380cb8458 Add brew upgrade step in CI 2020-09-10 19:14:33 +02:00
Jonas Kvinge
48e0e6af71 Ignore Radio Paradise "commercial" break for cover and lyrics search 2020-09-10 17:17:55 +02:00
Jonas Kvinge
b756bccc7a Link to iconv to fix compile on Windows 2020-09-10 17:14:14 +02:00
Jonas Kvinge
ae8eed7a67 Add ffmpeg to nsi 2020-09-10 17:12:18 +02:00
Strawbs Bot
ae4882bec3 Update translations 2020-09-10 01:03:26 +02:00
Strawbs Bot
1379741859 Update translations 2020-09-09 01:02:25 +02:00
Jonas Kvinge
b45c7ace78 Fix show equalizer signal slot connect 2020-09-08 18:12:18 +02:00
Strawbs Bot
3843d9f55b Update translations 2020-09-06 01:02:20 +02:00
Jonas Kvinge
f3422cb2fe Call raise() on dialogs to make sure they are on top
Fixes #535
2020-09-05 19:54:21 +02:00
Jonas Kvinge
73692797dc Fix QWidget::enterEvent with Qt 6 2020-09-05 19:20:43 +02:00
Jonas Kvinge
31dd910289 Make sure albums in cover manager are unique
Fixes #529
2020-09-05 18:59:35 +02:00
Jonas Kvinge
da51580299 Fix year - album collection grouping
Fixes #534
2020-09-05 17:26:42 +02:00
Strawbs Bot
9db148b1ec Update translations 2020-09-05 01:03:16 +02:00
Jonas Kvinge
091b1b8209 Use QKeyCombination with Qt 6 2020-09-04 23:00:42 +02:00
Jonas Kvinge
900a4071ed Use bytearray for qChecksum with Qt 6 2020-09-04 21:54:59 +02:00
Jonas Kvinge
df4b2f7938 Add icon for edit tag playlist right click menu actions
Fixes #531
2020-09-04 20:55:57 +02:00
Jonas Kvinge
803f44d9bc Disable use system icons setting for macOS and Windows
Fixes #532
2020-09-04 20:51:43 +02:00
Jonas Kvinge
4b67aee020 Use | operator for QShortcut 2020-09-04 20:43:02 +02:00
Jonas Kvinge
71dc47d6c9 Use deleteLater() when destroying lazy initialized objects
Fixes #530
2020-09-04 20:25:46 +02:00
Strawbs Bot
9cb305fb0d Update translations 2020-09-04 01:02:02 +02:00
Jonas Kvinge
1672130486 Merge compilation off/off
Fixes #528
2020-09-03 19:20:59 +02:00
Jonas Kvinge
8e135e3e79 Set compilation for "Various Artists" in album artist tag too 2020-09-03 19:20:11 +02:00
Jonas Kvinge
aa1a4f6ea5 Update verify icons script 2020-09-03 17:04:40 +02:00
Jonas Kvinge
ba34cf5258 Possible fix for crash when deleting queued songs from playlist
See #527
2020-09-03 16:59:18 +02:00
Strawbs Bot
647089d2a8 Update translations 2020-09-03 01:02:25 +02:00
Jonas Kvinge
5211508eb4 Add multimedia-player-ipod-standard-black.png 2020-09-02 23:20:01 +02:00
Jonas Kvinge
e6f05ae465 Make sure icon exists in GuessIconForPath 2020-09-02 20:40:07 +02:00
Jonas Kvinge
a9193f9b76 Move itdb_device_free 2020-09-02 20:03:22 +02:00
Jonas Kvinge
e373a17cd3 Fallback to device-ipod for ipod icon name 2020-09-02 19:36:47 +02:00
Jonas Kvinge
ebab9b7e4a Delete deviceinfo using parent object 2020-09-02 19:35:13 +02:00
Jonas Kvinge
6de0399807 Guess icon for device in udisks2 lister 2020-09-02 19:34:46 +02:00
Strawbs Bot
5cc7bb80f6 Update translations 2020-09-02 01:11:51 +02:00
Jonas Kvinge
6e0bd9b3f8 Add override to LastFMImportDialog::closeEvent() 2020-09-01 22:27:34 +02:00
Jonas Kvinge
d1943f72d3 Remove use of Qt::AA_UseHighDpiPixmaps with Qt 6 2020-09-01 21:05:07 +02:00
Jonas Kvinge
81709873bd Use art_manual for itdb thumbnail 2020-09-01 20:59:19 +02:00
Jonas Kvinge
f6bb7cd8ed Only include Dmg.cmake on macOS 2020-09-01 20:40:14 +02:00
Jonas Kvinge
d1c19e431c Add check for gdk-pixbuf-2.0 2020-09-01 20:27:05 +02:00
Jonas Kvinge
9ab2dde8ab Read tumbnails from itdb tracks 2020-09-01 01:04:01 +02:00
Jonas Kvinge
71559bb125 Turn pretty covers on by default in collection model 2020-09-01 01:02:02 +02:00
Strawbs Bot
ae4c95262c Update translations 2020-09-01 01:01:45 +02:00
Jonas Kvinge
dbbf07c9c1 Specify JPG when saving cover to temp file for iPod's 2020-08-31 20:13:53 +02:00
Jonas Kvinge
ab8cd619d5 Save cover image to file before copying to iPod
Fixes #519
2020-08-31 18:38:54 +02:00
Jonas Kvinge
c30ad2819d Disable use of HTML in system tray tooltip on Cinnamon 2020-08-31 18:06:37 +02:00
Jonas Kvinge
311e91797a Update ISSUE_TEMPLATE.md 2020-08-31 17:53:46 +02:00
Jonas Kvinge
52be4df355 Use only one issue template 2020-08-31 17:52:16 +02:00
Jonas Kvinge
0364e81050 Remove mageia from Circle CI 2020-08-31 17:15:31 +02:00
Jonas Kvinge
2d49b71bc9 Read song creation time from subsonic API
Fixes #526
2020-08-31 17:05:09 +02:00
Jonas Kvinge
a18a4bdf31 Set wordwrap 2020-08-31 16:25:01 +02:00
Strawbs Bot
d3acbe8288 Update translations 2020-08-31 01:04:42 +02:00
Jonas Kvinge
22afcbcbb6 Only allow playlist editing if song is editable
Set proper flags in model instead of overriding edit in view.

Proper fix for #524
2020-08-30 22:23:38 +02:00
Jonas Kvinge
495c6bc21c Remove unused StyleHelper::drawIconWithShadow function 2020-08-30 21:51:26 +02:00
Jonas Kvinge
cfd1fe59f3 Only allow playlist editing if song is editable.
Fixes #524
2020-08-30 21:40:04 +02:00
Jonas Kvinge
c46cf5bc84 Check for missing lyrics 2020-08-30 21:13:31 +02:00
Jonas Kvinge
a8742557bd Add lyrics from fandom.com 2020-08-30 21:06:58 +02:00
Jonas Kvinge
3bea58cf56 Fix resetting last.fm import dialog on close when finished 2020-08-30 18:28:51 +02:00
Jonas Kvinge
5aaa5231b8 Add Last.fm import
Fixes #247
2020-08-30 18:09:13 +02:00
Jonas Kvinge
82d10dd7cb Remove debug line 2020-08-30 01:59:26 +02:00
Jonas Kvinge
841065fb91 Load icons for buttons before setting enabled/disabled
Fixes #500
2020-08-30 01:57:21 +02:00
Strawbs Bot
c7146ef138 Update translations 2020-08-30 01:02:19 +02:00
Jonas Kvinge
08f32d1de6 Refactor playlist view/header code
- Don't reload all settings when changing playlists
- Fix initial playlist header columns sizes
- Properly reset header state when resetting columns
2020-08-29 19:55:00 +02:00
Jonas Kvinge
4c3f86aa4d Remove use of Qt::AA_EnableHighDpiScaling with Qt 6 2020-08-29 19:18:56 +02:00
Jonas Kvinge
445cf22333 Use deleteLater 2020-08-29 16:24:40 +02:00
Strawbs Bot
4919de647a Update translations 2020-08-28 01:01:40 +02:00
Jonas Kvinge
77fae99528 Fix spacing for sources in scrobbling settings
Fixes #523
2020-08-27 23:36:09 +02:00
Jonas Kvinge
10bd4b40b9 Switch branch for macOS upload 2020-08-27 22:51:34 +02:00
Jonas Kvinge
9d8e6bb253 Merge branch 'macos' into master 2020-08-27 22:50:11 +02:00
Jonas Kvinge
49c71ecfad Update macOS CI 2020-08-27 22:11:19 +02:00
Strawbs Bot
d18834b415 Update translations 2020-08-27 01:02:03 +02:00
Jonas Kvinge
e52cda193e Replace QAbstractItemView::viewOptions with initViewItemOption 2020-08-26 23:35:33 +02:00
Jonas Kvinge
0d5edd35ea Register ColumnAlignmentMap 2020-08-26 22:36:09 +02:00
Strawbs Bot
f3f51c3d9d Update translations 2020-08-26 01:08:17 +02:00
Jonas Kvinge
1431916183 Add setting for enabling scrobbling based on song source
Fixes #416
2020-08-25 23:44:27 +02:00
Jonas Kvinge
ae48008803 Remove use of qRegisterMetaTypeStreamOperators with Qt 6 2020-08-25 22:48:21 +02:00
Jonas Kvinge
5664813931 Remove Mageia from GitHub CI 2020-08-25 21:57:09 +02:00
Jonas Kvinge
3948af80b8 Fix pixelated source icon for currently playing song in playlist 2020-08-25 21:51:23 +02:00
Strawbs Bot
343d6f9aff Update translations 2020-08-24 01:03:49 +02:00
Jonas Kvinge
51b379502f Always use qint32 for QDBusArgument 2020-08-23 21:55:34 +02:00
Jonas Kvinge
82142751de Improve playlist autoscrolling
Fixes #420
2020-08-23 19:37:24 +02:00
Jonas Kvinge
4e5755f218 Refactor some functions 2020-08-23 19:17:50 +02:00
Jonas Kvinge
2f4f29517e Create new contructor for UrlHandler 2020-08-23 18:52:30 +02:00
Strawbs Bot
e8a073651f Update translations 2020-08-23 05:53:13 +02:00
Jonas Kvinge
d23da7a612 Replace Qt::MidButton with Qt::MiddleButton 2020-08-23 03:27:24 +02:00
Jonas Kvinge
1ae4938da3 Register D-Bus metatype for QImage if system has D-Bus 2020-08-23 03:20:53 +02:00
Jonas Kvinge
b5e27d4d69 Declare QDBusArgument for QImage in osddbus.h 2020-08-23 03:19:40 +02:00
Jonas Kvinge
d74fc8d1ce Fix macoOS builds in Travis CI 2020-08-22 14:02:12 +02:00
Jonas Kvinge
c5d9a41967 Remove tumbleweed from CircleCI 2020-08-22 00:27:12 +02:00
Jonas Kvinge
7e3042c4f4 Remove AA_DontShowIconsInMenus
Possible fix for #516
2020-08-21 23:40:44 +02:00
Jonas Kvinge
1291dadbd6 Replace use of QStringRef 2020-08-21 22:39:01 +02:00
Jonas Kvinge
f645099a39 Remove FMPS parser 2020-08-21 22:38:40 +02:00
Jonas Kvinge
8f32038891 Add missing musicstorage include 2020-08-19 22:35:35 +02:00
Jonas Kvinge
a6fb1dcdf9 Add missing QWindow include 2020-08-19 22:35:22 +02:00
Jonas Kvinge
f01b469f3f Allow to delete files permanently in fileview with Qt < 5.15 2020-08-19 22:25:05 +02:00
Jonas Kvinge
47884919c8 Only show delete files option in setting with Qt >= 5.15 2020-08-19 22:11:03 +02:00
Jonas Kvinge
653a35496d Add optional delete from disk in collection and playlist
Fixes #284
2020-08-19 22:02:35 +02:00
Jonas Kvinge
9b14df6b27 Use static_cast in QComboBox::findData 2020-08-16 18:22:10 +02:00
Jonas Kvinge
09813f3c5a Turn back git revision 2020-08-16 03:02:27 +02:00
Jonas Kvinge
7d76b7e4c2 Update protobuf version in nsi 2020-08-15 23:15:51 +02:00
403 changed files with 35092 additions and 13130 deletions

View File

@@ -226,66 +226,6 @@ commands:
gstreamer1-plugins-base-devel
install_mageia_dependencies:
description: Install Mageia dependencies
steps:
- run:
name: Update packages
command: urpmi.update --auto -a
- run:
name: Configure auto update
command: urpmi --auto --auto-update
- run:
name: Install dependencies
command: >
urpmi --auto --force
urpmi-debuginfo-install
git
tar
rpmdevtools
make
cmake
glibc
binutils
gcc-c++
man
gettext
notification-daemon
dbus-devel
libgnutls-devel
lib64boost-devel
lib64protobuf-devel
protobuf-compiler
lib64sqlite3-devel
lib64alsa2-devel
lib64pulseaudio-devel
lib64notify-devel
lib64qt5core-devel
lib64qt5gui-devel
lib64qt5widgets-devel
lib64qt5network-devel
lib64qt5concurrent-devel
lib64qt5sql-devel
lib64qt5dbus-devel
lib64qt5x11extras-devel
lib64qt5help-devel
libqt5test-devel
lib64gstreamer1.0-devel
lib64gstreamer-plugins-base1.0-devel
lib64cdio-devel
lib64gpod-devel
lib64mtp-devel
lib64raw1394-devel
lib64chromaprint-devel
libfftw-devel
desktop-file-utils
appstream-util
libappstream-glib8
hicolor-icon-theme
qt5ct
lib64mesaegl1
install_debian_dependencies:
description: Install Debian dependencies
steps:
@@ -294,6 +234,7 @@ commands:
command: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -342,6 +283,7 @@ commands:
command: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -392,24 +334,6 @@ jobs:
- build_source
build_opensuse_tumbleweed:
docker:
- image: opensuse/tumbleweed
environment:
RPM_BUILD_NCPUS: "2"
steps:
- run:
name: Update packages
command: zypper --non-interactive --gpg-auto-import-keys ref
- run:
name: Upgrade packages
command: zypper --non-interactive --gpg-auto-import-keys dup
- install_opensuse_dependencies
- checkout
- cmake
- build_source
- build_rpm
build_opensuse_lp151:
docker:
- image: opensuse/leap:15.1
@@ -473,19 +397,6 @@ jobs:
- build_rpm
build_mageia_7:
docker:
- image: mageia:7
environment:
RPM_BUILD_NCPUS: "2"
steps:
- install_mageia_dependencies
- checkout
- cmake
- build_source
- build_rpm
build_debian_buster:
docker:
- image: debian:buster
@@ -543,10 +454,6 @@ workflows:
only: /.*/
- build_opensuse_tumbleweed:
filters:
tags:
only: /.*/
- build_opensuse_lp151:
filters:
tags:
@@ -567,12 +474,6 @@ workflows:
only: /.*/
- build_mageia_7:
filters:
tags:
only: /.*/
- build_centos_8:
filters:
tags:

105
.clang-format Normal file
View File

@@ -0,0 +1,105 @@
BasedOnStyle: Chromium
Language: Cpp
Standard: Cpp11
AccessModifierOffset: -1
AlignAfterOpenBracket: false
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignConsecutiveMacros: true
AlignEscapedNewlines: true
AlignOperands: false
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: true
AllowShortIfStatementsOnASingleLine: true
AllowShortLambdasOnASingleLine: true
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: No
BinPackArguments: true
BinPackParameters: true
BreakBeforeBraces: false
BreakBeforeBinaryOperators: false
BreakBeforeBraces: Stroustrup
BreakBeforeTernaryOperators: false
BreakConstructorInitializers: BeforeColon
BreakInheritanceList: BeforeColon
BreakStringLiterals: false
ColumnLimit: 0
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 2
Cpp11BracedListStyle: false
DeriveLineEnding: true
DerivePointerAlignment: true
DisableFormat: false
ExperimentalAutoDetectBinPacking: true
FixNamespaceComments: true
IncludeBlocks: Preserve
IndentCaseLabels: true
IndentGotoLabels: true
IndentPPDirectives: AfterHash
IndentWidth: 2
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: true
MaxEmptyLinesToKeep: 100
NamespaceIndentation: None
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: false
PenaltyBreakAssignment: 0
PenaltyBreakBeforeFirstCallParameter: 0
PenaltyBreakComment: 0
PenaltyBreakFirstLessLess: 0
PenaltyBreakString: 0
PenaltyBreakTemplateDeclaration: 0
PenaltyExcessCharacter: 0
PenaltyReturnTypeOnItsOwnLine: 0
PointerAlignment: Right
ReflowComments: false
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInParentheses: false
SpacesInSquareBrackets: false
UseCRLF: false
UseTab: Never
BreakBeforeBraces: Custom
BraceWrapping:
AfterFunction: false
AfterCaseLabel: false
AfterStruct: false
AfterClass: false
AfterEnum: false
AfterUnion: false
AfterControlStatement: Never
AfterNamespace: false
AfterObjCDeclaration: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: true
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false

View File

@@ -7,7 +7,12 @@ assignees: ''
---
For general technical questions and help with technical issues please use the forum on https://forum.strawberrymusicplayer.org/
For technical issues, questions and feature suggestions/requests please use the forum on https://forum.strawberrymusicplayer.org/
Check the Changelog to see if the issue is already fixed:
https://github.com/strawberrymusicplayer/strawberry/blob/master/Changelog
If it's fixed, try the latest development build from: https://builds.strawberrymusicplayer.org/
**Describe the bug**
A clear and concise description of what the bug is.

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
@@ -367,7 +366,7 @@ jobs:
- name: Configure CMake
shell: bash
working-directory: build
run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DWITH_QT6=ON
run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DBUILD_WITH_QT6=ON
- name: Build
working-directory: build
run: cmake --build . --config $BUILD_TYPE
@@ -451,6 +450,160 @@ jobs:
run: rpmbuild -ba ../dist/unix/strawberry.spec
build_fedora_33:
name: Build Fedora 33
runs-on: ubuntu-latest
container:
image: fedora:33
env:
RPM_BUILD_NCPUS: "2"
steps:
- uses: actions/checkout@v1.2.0
- name: Update packages
run: yum update --assumeyes
- name: Upgrade packages
run: yum upgrade --assumeyes
- name: Install Fedora dependencies
run: >
dnf install --assumeyes
@development-tools
redhat-lsb-core
git
glibc
gcc-c++
rpmdevtools
make
cmake
pkgconfig
glib
man
tar
gettext
openssh
boost-devel
dbus-devel
protobuf-devel
protobuf-compiler
sqlite-devel
alsa-lib-devel
pulseaudio-libs-devel
libnotify-devel
gnutls-devel
qt5-qtbase-devel
qt5-qtx11extras-devel
qt5-qttools-devel
gstreamer1-devel
gstreamer1-plugins-base-devel
taglib-devel
libcdio-devel
libgpod-devel
libmtp-devel
libchromaprint-devel
fftw-devel
desktop-file-utils
libappstream-glib
hicolor-icon-theme
- name: Create Build Environment
shell: bash
run: cmake -E make_directory build
- name: Configure CMake
shell: bash
working-directory: build
run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE
- name: Create source tarball
working-directory: build
run: ../dist/scripts/maketarball.sh
- name: Create RPM build sources directories
working-directory: build
run: mkdir -p ~/rpmbuild/SOURCES /usr/src/packages/SOURCES
- name: Copy source tarball
working-directory: build
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
- name: Build RPM
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
build_fedora_34:
name: Build Fedora 34
runs-on: ubuntu-latest
container:
image: fedora:34
env:
RPM_BUILD_NCPUS: "2"
steps:
- uses: actions/checkout@v1.2.0
- name: Update packages
run: yum update --assumeyes
- name: Upgrade packages
run: yum upgrade --assumeyes
- name: Install Fedora dependencies
run: >
dnf install --assumeyes
@development-tools
redhat-lsb-core
git
glibc
gcc-c++
rpmdevtools
make
cmake
pkgconfig
glib
man
tar
gettext
openssh
boost-devel
dbus-devel
protobuf-devel
protobuf-compiler
sqlite-devel
alsa-lib-devel
pulseaudio-libs-devel
libnotify-devel
gnutls-devel
qt5-qtbase-devel
qt5-qtx11extras-devel
qt5-qttools-devel
gstreamer1-devel
gstreamer1-plugins-base-devel
taglib-devel
libcdio-devel
libgpod-devel
libmtp-devel
libchromaprint-devel
fftw-devel
desktop-file-utils
libappstream-glib
hicolor-icon-theme
- name: Create Build Environment
shell: bash
run: cmake -E make_directory build
- name: Configure CMake
shell: bash
working-directory: build
run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE
- name: Create source tarball
working-directory: build
run: ../dist/scripts/maketarball.sh
- name: Create RPM build sources directories
working-directory: build
run: mkdir -p ~/rpmbuild/SOURCES /usr/src/packages/SOURCES
- name: Copy source tarball
working-directory: build
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
- name: Build RPM
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
build_centos_8:
name: Build CentOS 8
runs-on: ubuntu-latest
@@ -541,89 +694,6 @@ jobs:
run: rpmbuild -ba ../dist/unix/strawberry.spec
build_mageia_7:
name: Build Mageia 7
runs-on: ubuntu-latest
container:
image: mageia:7
steps:
- uses: actions/checkout@v1.2.0
- name: Update packages
run: urpmi.update --auto -a
- name: Configure auto update
run: urpmi --auto --auto-update
- name: Install Mageia dependencies
run: >
urpmi --auto --force
urpmi-debuginfo-install
git
tar
rpmdevtools
make
cmake
glibc
binutils
gcc-c++
man
gettext
notification-daemon
dbus-devel
libgnutls-devel
lib64boost-devel
lib64protobuf-devel
protobuf-compiler
lib64sqlite3-devel
lib64alsa2-devel
lib64pulseaudio-devel
lib64notify-devel
lib64qt5core-devel
lib64qt5gui-devel
lib64qt5widgets-devel
lib64qt5network-devel
lib64qt5concurrent-devel
lib64qt5sql-devel
lib64qt5dbus-devel
lib64qt5x11extras-devel
lib64qt5help-devel
libqt5test-devel
lib64gstreamer1.0-devel
lib64gstreamer-plugins-base1.0-devel
lib64cdio-devel
lib64gpod-devel
lib64mtp-devel
lib64raw1394-devel
lib64chromaprint-devel
libfftw-devel
desktop-file-utils
appstream-util
libappstream-glib8
hicolor-icon-theme
qt5ct
lib64mesaegl1
- name: Create Build Environment
shell: bash
run: cmake -E make_directory build
- name: Configure CMake
shell: bash
working-directory: build
run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE
- name: Create source tarball
working-directory: build
run: ../dist/scripts/maketarball.sh
- name: Create RPM build sources directories
working-directory: build
run: mkdir -p ~/rpmbuild/SOURCES /usr/src/packages/SOURCES
- name: Copy source tarball
working-directory: build
run: cp strawberry-*.tar.xz ~/rpmbuild/SOURCES/
- name: Build RPM
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
build_debian_buster:
name: Build Debian Buster
runs-on: ubuntu-latest
@@ -635,6 +705,7 @@ jobs:
run: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -691,6 +762,7 @@ jobs:
run: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -749,6 +821,7 @@ jobs:
run: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -810,6 +883,7 @@ jobs:
run: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -871,6 +945,7 @@ jobs:
run: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -924,8 +999,10 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v1.2.0
- name: Unlink python
run: brew unlink python@2
#- name: Update
# run: brew update
#- name: Upgrade
# run: brew upgrade
- name: Install packages
run: >
brew install
@@ -977,10 +1054,14 @@ jobs:
working-directory: build
shell: bash
run: make install
#- name: Create DMG
# working-directory: build
# shell: bash
# run: make dmg
- name: Create DMG
working-directory: build
shell: bash
run: make dmg
- uses: actions/upload-artifact@v2
with:
name: upload-macos
path: build/strawberry-*.dmg
build-windows:
@@ -1019,10 +1100,6 @@ jobs:
working-directory: build
run: make -j2
- name: Strip executables
working-directory: build
run: /usr/src/strawberry-mxe/usr/bin/x86_64-w64-mingw32.shared-strip *.exe
- name: Create directories
working-directory: build
run: mkdir -p gio-modules platforms sqldrivers imageformats styles gstreamer-plugins nsisplugins
@@ -1099,13 +1176,9 @@ jobs:
/usr/src/strawberry-mxe/usr/x86_64-w64-mingw32.shared/bin/gstreamer-1.0/libgstlibav.dll
${GITHUB_WORKSPACE}/build/gstreamer-plugins/
- name: Copy killproc.exe
- name: Copy extra binaries
working-directory: build
run: cp /usr/src/strawberry-mxe/usr/x86_64-w64-mingw32.shared/bin/killproc.exe .
- name: Copy liborc-0.4-0.dll
working-directory: build
run: cp /usr/src/strawberry-mxe/usr/x86_64-w64-mingw32.shared/bin/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
@@ -1121,6 +1194,10 @@ jobs:
-F ./gstreamer-plugins
-R /usr/src/strawberry-mxe/usr/x86_64-w64-mingw32.shared
- name: Strip binaries
working-directory: build
run: find . -type f \( -iname \*.dll -o -iname \*.exe \) -exec /usr/src/strawberry-mxe/usr/bin/x86_64-w64-mingw32.shared-strip {} \;
- name: Copy nsis files
working-directory: build
run: cp ${GITHUB_WORKSPACE}/dist/windows/*.nsi ${GITHUB_WORKSPACE}/dist/windows/*.nsh ${GITHUB_WORKSPACE}/dist/windows/*.ico .
@@ -1128,3 +1205,27 @@ jobs:
- name: Build Windows installer
working-directory: build
run: makensis strawberry.nsi
upload-macos:
name: Upload macOS DMG
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
needs:
- build-macos
steps:
- uses: actions/checkout@v1.2.0
- uses: actions/download-artifact@v2
with:
path: uploads
- name: Install SSH keys
uses: shimataro/ssh-key-action@v2
with:
known_hosts: ${{ secrets.KNOWN_HOSTS2 }}
key: ${{ secrets.SSH_KEY }}
- name: rsync
run: |
set -x
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

View File

@@ -2,8 +2,6 @@ sudo: required
language: C++
os:
- osx
services:
- docker
compiler:
- gcc
@@ -12,45 +10,36 @@ before_install:
echo $DEPLOY_KEY_ENC | base64 --decode | openssl aes-256-cbc -K $encrypted_83a41ac424a6_key -iv $encrypted_83a41ac424a6_iv -out ~/.ssh/id_rsa -d ;
chmod 600 ~/.ssh/id_rsa ;
fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
docker build -f Dockerfile -t strawberry-build . || travis_terminate 1;
docker run --name build -itd strawberry-build /bin/bash || travis_terminate 1;
docker exec build git clone https://github.com/strawberrymusicplayer/strawberry;
fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
git fetch --unshallow || travis_terminate 1;
git pull || travis_terminate 1;
brew unlink python@2 || travis_terminate 1;
brew install glib pkgconfig libffi protobuf protobuf-c qt gettext gnutls fftw sqlite chromaprint zlib taglib;
brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav;
brew install libcdio libmtp;
brew install create-dmg;
brew cask install sparkle;
sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1)/Sparkle.framework /Library/Frameworks/Sparkle.framework;
sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1)/Sparkle.framework.dSYM /Library/Frameworks/Sparkle.framework.dSYM;
export Qt5_DIR=/usr/local/opt/qt5/lib/cmake;
export Qt5LinguistTools_DIR=/usr/local/opt/qt5/lib/cmake/Qt5LinguistTools;
export PATH="/usr/local/opt/gettext/bin:$PATH";
export PKG_CONFIG_PATH="/usr/local/opt/libffi/lib/pkgconfig/:/usr/local/opt/zlib/lib/pkgconfig:$PKG_CONFIG_PATH";
ls /usr/local/lib/gstreamer-1.0;
fi
- git fetch --unshallow
- git pull
- brew update
- travis_wait 120 brew upgrade || echo "Failed"
- travis_wait 120 brew upgrade || echo "Failed"
- brew install glib pkgconfig libffi protobuf protobuf-c qt gettext gnutls fftw sqlite chromaprint zlib taglib
- brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav
- brew install libcdio libmtp
- brew install create-dmg
- brew cask install sparkle
- sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1)/Sparkle.framework /Library/Frameworks/Sparkle.framework
- sudo ln -s /usr/local/Caskroom/sparkle/$(ls /usr/local/Caskroom/sparkle | head -n1)/Sparkle.framework.dSYM /Library/Frameworks/Sparkle.framework.dSYM
- export Qt5_DIR=/usr/local/opt/qt5/lib/cmake
- export Qt5LinguistTools_DIR=/usr/local/opt/qt5/lib/cmake/Qt5LinguistTools
- ls /usr/local/lib/gstreamer-1.0
before_script:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker exec build cmake -Hstrawberry -Bbuild ; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mkdir build; cd build; cmake .. -DUSE_BUNDLE=ON ; fi
- mkdir build
- cd build
- cmake .. -DUSE_BUNDLE=ON
script:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker exec build make -C build -j8 ; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
make -j8 || travis_terminate 1;
make install || travis_terminate 1;
make dmg;
fi
- make -j8
- make install
- make dmg
after_success:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then ls -lh strawberry*.dmg; fi
- ls -lh strawberry*.dmg
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$CC_FOR_BUILD" == "gcc" ]] && [ -f ~/.ssh/id_rsa ]; then
if [[ "$TRAVIS_BRANCH" == "master" ]]; then
rsync -e "ssh -o StrictHostKeyChecking=no" -va strawberry*.dmg travis@echoes.jkvinge.net:/home/travis/builds/macos;
rsync -e "ssh -o StrictHostKeyChecking=no" -va strawberry*.dmg travis@echoes.jkvinge.net:/home/travis/builds/macos/highsierra/;
elif [[ "$TRAVIS_BRANCH" == "macos" ]]; then
rsync -e "ssh -o StrictHostKeyChecking=no" -va strawberry*.dmg travis@echoes.jkvinge.net:/home/travis/builds/macos;
rsync -e "ssh -o StrictHostKeyChecking=no" -va strawberry*.dmg travis@echoes.jkvinge.net:/home/travis/builds/macos/highsierra/;
fi
fi

View File

@@ -10,17 +10,12 @@ endif()
set(SINGLEAPP-SOURCES singleapplication.cpp singleapplication_p.cpp)
set(SINGLEAPP-MOC-HEADERS singleapplication.h singleapplication_p.h)
if(WITH_QT6)
if(BUILD_WITH_QT6)
qt6_wrap_cpp(SINGLEAPP-SOURCES-MOC ${SINGLEAPP-MOC-HEADERS})
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}
@@ -33,16 +28,12 @@ target_link_libraries(singleapplication PRIVATE
set(SINGLECOREAPP-SOURCES singlecoreapplication.cpp singlecoreapplication_p.cpp)
set(SINGLECOREAPP-MOC-HEADERS singlecoreapplication.h singlecoreapplication_p.h)
if(WITH_QT6)
if(BUILD_WITH_QT6)
qt6_wrap_cpp(SINGLECOREAPP-SOURCES-MOC ${SINGLECOREAPP-MOC-HEADERS})
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,6 +1,6 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2018
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -35,113 +35,131 @@
#include <limits>
#include <QtGlobal>
#include <QCoreApplication>
#include <QApplication>
#include <QThread>
#include <QSharedMemory>
#include <QLocalSocket>
#include <QByteArray>
#include <QElapsedTimer>
#include <QtDebug>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#else
# include <QDateTime>
#endif
#include "singleapplication.h"
#include "singleapplication_p.h"
/**
* @brief Constructor.
* Checks and fires up LocalServer or closes the program if another instance already exists
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
* @param argc
* @param argv
* @param {bool} allowSecondaryInstances
* @param allowSecondary Whether to enable secondary instance support
* @param options Optional flags to toggle specific behaviour
* @param timeout Maximum time blocking functions are allowed during app load
*/
SingleApplication::SingleApplication(int &argc, char *argv[], bool allowSecondary, Options options, int timeout)
: app_t(argc, argv), d_ptr(new SingleApplicationPrivate(this)) {
: app_t(argc, argv),
d_ptr(new SingleApplicationPrivate(this)) {
Q_D(SingleApplication);
// Store the current mode of the program
d->options = options;
d->options_ = options;
// Generating an application ID used for identifying the shared memory block and QLocalServer
d->genBlockServerName();
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
d->randomSleep();
#ifdef Q_OS_UNIX
// By explicitly attaching it and then deleting it we make sure that the
// memory is deleted even after the process has crashed on Unix.
d->memory = new QSharedMemory(d->blockServerName);
d->memory->attach();
delete d->memory;
// By explicitly attaching it and then deleting it we make sure that the memory is deleted even after the process has crashed on Unix.
d->memory_ = new QSharedMemory(d->blockServerName_);
d->memory_->attach();
delete d->memory_;
#endif
// Guarantee thread safe behaviour with a shared memory block.
d->memory = new QSharedMemory(d->blockServerName);
d->memory_ = new QSharedMemory(d->blockServerName_);
// Create a shared memory block
if (d->memory->create(sizeof(InstancesInfo))) {
if (d->memory_->create(sizeof(InstancesInfo))) {
// Initialize the shared memory block
d->memory->lock();
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory block after create.";
abortSafely();
}
d->initializeMemoryBlock();
d->memory->unlock();
}
else {
// Attempt to attach to the memory segment
if (! d->memory->attach()) {
qCritical() << "SingleApplication: Unable to attach to shared memory block.";
qCritical() << d->memory->errorString();
delete d;
::exit(EXIT_FAILURE);
if (d->memory_->error() == QSharedMemory::AlreadyExists) {
// Attempt to attach to the memory segment
if (!d->memory_->attach()) {
qCritical() << "SingleApplication: Unable to attach to shared memory block.";
abortSafely();
}
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory block after attach.";
abortSafely();
}
}
else {
qCritical() << "SingleApplication: Unable to create block.";
abortSafely();
}
}
InstancesInfo* inst = static_cast<InstancesInfo*>(d->memory->data());
InstancesInfo *inst = static_cast<InstancesInfo*>(d->memory_->data());
QElapsedTimer time;
time.start();
// Make sure the shared memory block is initialised and in consistent state
while (true) {
d->memory->lock();
forever {
// If the shared memory block's checksum is valid continue
if (d->blockChecksum() == inst->checksum) break;
// If more than 5s have elapsed, assume the primary instance crashed and assume it's position
if (time.elapsed() > 5000) {
qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure.";
d->initializeMemoryBlock();
}
d->memory->unlock();
// Random sleep here limits the probability of a collision between two racing apps
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QThread::sleep(QRandomGenerator::global()->bounded(8u, 18u));
#else
qsrand(QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max());
QThread::sleep(8 + static_cast<unsigned long>(static_cast <float>(qrand()) / RAND_MAX * 10));
#endif
// Otherwise wait for a random period and try again.
// The random sleep here limits the probability of a collision between two racing apps and allows the app to initialise faster
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory for random wait.";
qDebug() << d->memory_->errorString();
}
d->randomSleep();
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory after random wait.";
abortSafely();
}
}
if (inst->primary == false) {
d->startPrimary();
d->memory->unlock();
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory after primary start.";
qDebug() << d->memory_->errorString();
}
return;
}
// Check if another instance can be started
if (allowSecondary) {
inst->secondary += 1;
inst->checksum = d->blockChecksum();
d->instanceNumber = inst->secondary;
d->startSecondary();
if (d->options & Mode::SecondaryNotification) {
if (d->options_ & Mode::SecondaryNotification) {
d->connectToPrimary(timeout, SingleApplicationPrivate::SecondaryInstance);
}
d->memory->unlock();
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory after secondary start.";
qDebug() << d->memory_->errorString();
}
return;
}
d->memory->unlock();
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory at end of execution.";
qDebug() << d->memory_->errorString();
}
d->connectToPrimary(timeout, SingleApplicationPrivate::NewInstance);
@@ -151,34 +169,73 @@ SingleApplication::SingleApplication(int &argc, char *argv[], bool allowSecondar
}
/**
* @brief Destructor
*/
SingleApplication::~SingleApplication() {
Q_D(SingleApplication);
delete d;
}
/**
* Checks if the current application instance is primary.
* @return Returns true if the instance is primary, false otherwise.
*/
bool SingleApplication::isPrimary() {
Q_D(SingleApplication);
return d->server != nullptr;
return d->server_ != nullptr;
}
/**
* Checks if the current application instance is secondary.
* @return Returns true if the instance is secondary, false otherwise.
*/
bool SingleApplication::isSecondary() {
Q_D(SingleApplication);
return d->server == nullptr;
return d->server_ == nullptr;
}
/**
* Allows you to identify an instance by returning unique consecutive instance ids.
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
* @return Returns a unique instance id.
*/
quint32 SingleApplication::instanceId() {
Q_D(SingleApplication);
return d->instanceNumber;
return d->instanceNumber_;
}
/**
* Returns the OS PID (Process Identifier) of the process running the primary instance.
* Especially useful when SingleApplication is coupled with OS. specific APIs.
* @return Returns the primary instance PID.
*/
qint64 SingleApplication::primaryPid() {
Q_D(SingleApplication);
return d->primaryPid();
}
/**
* Returns the username the primary instance is running as.
* @return Returns the username the primary instance is running as.
*/
QString SingleApplication::primaryUser() {
Q_D(SingleApplication);
return d->primaryUser();
}
/**
* Returns the username the current instance is running as.
* @return Returns the username the current instance is running as.
*/
QString SingleApplication::currentUser() {
Q_D(SingleApplication);
return d->getUsername();
}
/**
* Sends message to the Primary Instance.
* @param message The message to send.
* @param timeout the maximum timeout in milliseconds for blocking functions.
* @return true if the message was sent successfuly, false otherwise.
*/
bool SingleApplication::sendMessage(QByteArray message, int timeout) {
Q_D(SingleApplication);
@@ -187,11 +244,26 @@ bool SingleApplication::sendMessage(QByteArray message, int timeout) {
if (isPrimary()) return false;
// Make sure the socket is connected
d->connectToPrimary(timeout, SingleApplicationPrivate::Reconnect);
if (!d->connectToPrimary(timeout, SingleApplicationPrivate::Reconnect))
return false;
d->socket->write(message);
bool dataWritten = d->socket->waitForBytesWritten(timeout);
d->socket->flush();
d->socket_->write(message);
const bool dataWritten = d->socket_->waitForBytesWritten(timeout);
d->socket_->flush();
return dataWritten;
}
/**
* Cleans up the shared memory block and exits with a failure.
* This function halts program execution.
*/
void SingleApplication::abortSafely() {
Q_D(SingleApplication);
qCritical() << "SingleApplication: " << d->memory_->error() << d->memory_->errorString();
delete d;
::exit(EXIT_FAILURE);
}

View File

@@ -1,6 +1,6 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2018
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -42,16 +42,15 @@
class SingleApplicationPrivate;
/**
* @brief The SingleApplication class handles multipe instances of the same
* Application
* @see QCoreApplication
* @brief The SingleApplication class handles multipe instances of the same Application
* @see QApplication
*/
class SingleApplication : public QApplication {
Q_OBJECT
typedef QApplication app_t;
public:
public:
/**
* @brief Mode of operation of SingleApplication.
* Whether the block should be user-wide or system-wide and whether the
@@ -63,11 +62,11 @@ public:
* @enum
*/
enum Mode {
User = 1 << 0,
System = 1 << 1,
SecondaryNotification = 1 << 2,
ExcludeAppVersion = 1 << 3,
ExcludeAppPath = 1 << 4
User = 1 << 0,
System = 1 << 1,
SecondaryNotification = 1 << 2,
ExcludeAppVersion = 1 << 3,
ExcludeAppPath = 1 << 4
};
Q_DECLARE_FLAGS(Options, Mode)
@@ -91,7 +90,7 @@ public:
* Usually 4*timeout would be the worst case (fail) scenario.
* @see See the corresponding QAPPLICATION_CLASS constructor for reference
*/
explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000 );
explicit SingleApplication(int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000);
~SingleApplication() override;
/**
@@ -118,6 +117,18 @@ public:
*/
qint64 primaryPid();
/**
* @brief Returns the username of the user running the primary instance
* @returns {QString}
*/
QString primaryUser();
/**
* @brief Returns the username of the current user
* @returns {QString}
*/
QString currentUser();
/**
* @brief Sends a message to the primary instance. Returns true on success.
* @param {int} timeout - Timeout for connecting
@@ -125,18 +136,18 @@ public:
* @note sendMessage() will return false if invoked from the primary
* instance.
*/
bool sendMessage( QByteArray message, int timeout = 1000 );
bool sendMessage(QByteArray message, int timeout = 1000);
signals:
void instanceStarted();
void receivedMessage( quint32 instanceId, QByteArray message );
void receivedMessage(quint32 instanceId, QByteArray message);
private:
SingleApplicationPrivate *d_ptr;
Q_DECLARE_PRIVATE(SingleApplication)
void abortSafely();
};
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options)
#endif // SINGLEAPPLICATION_H
#endif // SINGLEAPPLICATION_H

View File

@@ -1,6 +1,6 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2018
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -44,6 +44,8 @@
# include <pwd.h>
#endif
#include <QObject>
#include <QThread>
#include <QIODevice>
#include <QSharedMemory>
#include <QByteArray>
@@ -52,6 +54,12 @@
#include <QLocalServer>
#include <QLocalSocket>
#include <QDir>
#include <QElapsedTimer>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#else
# include <QDateTime>
#endif
#include "singleapplication.h"
#include "singleapplication_p.h"
@@ -61,33 +69,73 @@
# include <lmcons.h>
#endif
SingleApplicationPrivate::SingleApplicationPrivate(SingleApplication *_q_ptr)
: q_ptr(_q_ptr),
memory(nullptr),
socket(nullptr),
server(nullptr),
instanceNumber(-1)
{}
SingleApplicationPrivate::SingleApplicationPrivate(SingleApplication *ptr)
: q_ptr(ptr),
memory_(nullptr),
socket_(nullptr),
server_(nullptr),
instanceNumber_(-1) {}
SingleApplicationPrivate::~SingleApplicationPrivate() {
if (socket != nullptr) {
socket->close();
delete socket;
if (socket_ != nullptr) {
socket_->close();
delete socket_;
socket_ = nullptr;
}
memory->lock();
InstancesInfo* inst = static_cast<InstancesInfo*>(memory->data());
if (server != nullptr) {
server->close();
delete server;
inst->primary = false;
inst->primaryPid = -1;
inst->checksum = blockChecksum();
}
memory->unlock();
if (memory_ != nullptr) {
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
if (server_ != nullptr) {
server_->close();
delete server_;
inst->primary = false;
inst->primaryPid = -1;
inst->primaryUser[0] = '\0';
inst->checksum = blockChecksum();
}
memory_->unlock();
delete memory;
delete memory_;
memory_ = nullptr;
}
}
QString SingleApplicationPrivate::getUsername() {
#ifdef Q_OS_UNIX
QString username;
#if defined(HAVE_GETEUID) && defined(HAVE_GETPWUID)
struct passwd *pw = getpwuid(geteuid());
if (pw) {
username = QString::fromLocal8Bit(pw->pw_name);
}
#endif
if (username.isEmpty()) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
username = qEnvironmentVariable("USER");
#else
username = QString::fromLocal8Bit(qgetenv("USER"));
#endif
}
return username;
#endif
#ifdef Q_OS_WIN
wchar_t username[UNLEN + 1];
// Specifies size of the buffer on input
DWORD usernameLength = UNLEN + 1;
if (GetUserNameW(username, &usernameLength)) {
return QString::fromWCharArray(username);
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
return qEnvironmentVariable("USERNAME");
#else
return QString::fromLocal8Bit(qgetenv("USERNAME"));
#endif
#endif
}
@@ -99,11 +147,11 @@ void SingleApplicationPrivate::genBlockServerName() {
appData.addData(SingleApplication::app_t::organizationName().toUtf8());
appData.addData(SingleApplication::app_t::organizationDomain().toUtf8());
if (!(options & SingleApplication::Mode::ExcludeAppVersion)) {
if (!(options_ & SingleApplication::Mode::ExcludeAppVersion)) {
appData.addData(SingleApplication::app_t::applicationVersion().toUtf8());
}
if (! (options & SingleApplication::Mode::ExcludeAppPath)) {
if (!(options_ & SingleApplication::Mode::ExcludeAppPath)) {
#ifdef Q_OS_WIN
appData.addData(SingleApplication::app_t::applicationFilePath().toLower().toUtf8());
#else
@@ -112,42 +160,22 @@ void SingleApplicationPrivate::genBlockServerName() {
}
// User level block requires a user specific data in the hash
if (options & SingleApplication::Mode::User) {
#ifdef Q_OS_UNIX
QByteArray username;
#if defined(HAVE_GETEUID) && defined(HAVE_GETPWUID)
struct passwd *pw = getpwuid(geteuid());
if (pw) {
username = pw->pw_name;
}
#endif
if (username.isEmpty()) username = qgetenv("USER");
appData.addData(username);
#endif
#ifdef Q_OS_WIN
wchar_t username [ UNLEN + 1 ];
// Specifies size of the buffer on input
DWORD usernameLength = UNLEN + 1;
if (GetUserNameW(username, &usernameLength)) {
appData.addData(QString::fromWCharArray(username).toUtf8());
}
else {
appData.addData(qgetenv("USERNAME"));
}
#endif
if (options_ & SingleApplication::Mode::User) {
appData.addData(getUsername().toUtf8());
}
// Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with server naming requirements.
blockServerName = appData.result().toBase64().replace("/", "_");
blockServerName_ = appData.result().toBase64().replace("/", "_");
}
void SingleApplicationPrivate::initializeMemoryBlock() {
InstancesInfo* inst = static_cast<InstancesInfo*>(memory->data());
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
inst->primary = false;
inst->secondary = 0;
inst->primaryPid = -1;
inst->primaryUser[0] = '\0';
inst->checksum = blockChecksum();
}
@@ -156,133 +184,161 @@ void SingleApplicationPrivate::startPrimary() {
Q_Q(SingleApplication);
// Successful creation means that no main process exists
// So we start a QLocalServer to listen for connections
QLocalServer::removeServer(blockServerName);
server = new QLocalServer();
// Restrict access to the socket according to the
// SingleApplication::Mode::User flag on User level or no restrictions
if (options & SingleApplication::Mode::User) {
server->setSocketOptions(QLocalServer::UserAccessOption);
}
else {
server->setSocketOptions(QLocalServer::WorldAccessOption);
}
server->listen(blockServerName);
QObject::connect(server, &QLocalServer::newConnection, this, &SingleApplicationPrivate::slotConnectionEstablished);
// Reset the number of connections
InstancesInfo* inst = static_cast <InstancesInfo*>(memory->data());
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
inst->primary = true;
inst->primaryPid = q->applicationPid();
qstrncpy(inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser));
inst->checksum = blockChecksum();
instanceNumber_ = 0;
// Successful creation means that no main process exists
// So we start a QLocalServer to listen for connections
QLocalServer::removeServer(blockServerName_);
server_ = new QLocalServer();
instanceNumber = 0;
// Restrict access to the socket according to the SingleApplication::Mode::User flag on User level or no restrictions
if (options_ & SingleApplication::Mode::User) {
server_->setSocketOptions(QLocalServer::UserAccessOption);
}
else {
server_->setSocketOptions(QLocalServer::WorldAccessOption);
}
server_->listen(blockServerName_);
QObject::connect(server_, &QLocalServer::newConnection, this, &SingleApplicationPrivate::slotConnectionEstablished);
}
void SingleApplicationPrivate::startSecondary() {}
void SingleApplicationPrivate::startSecondary() {
void SingleApplicationPrivate::connectToPrimary(const int msecs, const ConnectionType connectionType) {
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
inst->secondary += 1;
inst->checksum = blockChecksum();
instanceNumber_ = inst->secondary;
}
bool SingleApplicationPrivate::connectToPrimary(int timeout, ConnectionType connectionType) {
QElapsedTimer time;
time.start();
// Connect to the Local Server of the Primary Instance if not already connected.
if (socket == nullptr) {
socket = new QLocalSocket();
if (socket_ == nullptr) {
socket_ = new QLocalSocket();
}
// If already connected - we are done;
if (socket->state() == QLocalSocket::ConnectedState)
return;
if (socket_->state() == QLocalSocket::ConnectedState) return true;
// If not connect
if (socket->state() == QLocalSocket::UnconnectedState ||
socket->state() == QLocalSocket::ClosingState) {
socket->connectToServer(blockServerName);
}
if (socket_->state() != QLocalSocket::ConnectedState) {
// Wait for being connected
if (socket->state() == QLocalSocket::ConnectingState) {
socket->waitForConnected(msecs);
forever {
randomSleep();
if (socket_->state() != QLocalSocket::ConnectingState)
socket_->connectToServer(blockServerName_);
if (socket_->state() == QLocalSocket::ConnectingState) {
socket_->waitForConnected(timeout - time.elapsed());
}
// If connected break out of the loop
if (socket_->state() == QLocalSocket::ConnectedState) break;
// If elapsed time since start is longer than the method timeout return
if (time.elapsed() >= timeout) return false;
}
}
// Initialisation message according to the SingleApplication protocol
if (socket->state() == QLocalSocket::ConnectedState) {
// Notify the parent that a new instance had been started;
QByteArray initMsg;
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
QByteArray initMsg;
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
writeStream.setVersion(QDataStream::Qt_5_8);
writeStream.setVersion(QDataStream::Qt_5_6);
writeStream << blockServerName_.toLatin1();
writeStream << static_cast<quint8>(connectionType);
writeStream << instanceNumber_;
writeStream << blockServerName.toLatin1();
writeStream << static_cast<quint8>(connectionType);
writeStream << instanceNumber;
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
writeStream << checksum;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
#else
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
#endif
// The header indicates the message length that follows
QByteArray header;
QDataStream headerStream(&header, QIODevice::WriteOnly);
writeStream << checksum;
headerStream.setVersion(QDataStream::Qt_5_6);
// The header indicates the message length that follows
QByteArray header;
QDataStream headerStream(&header, QIODevice::WriteOnly);
headerStream.setVersion(QDataStream::Qt_5_8);
headerStream << static_cast<quint64>(initMsg.length());
headerStream << static_cast <quint64>(initMsg.length());
socket_->write(header);
socket_->write(initMsg);
bool result = socket_->waitForBytesWritten(timeout - time.elapsed());
socket_->flush();
socket->write(header);
socket->write(initMsg);
socket->flush();
socket->waitForBytesWritten(msecs);
}
return result;
}
quint16 SingleApplicationPrivate::blockChecksum() {
return qChecksum(static_cast <const char *>(memory->data()), offsetof(InstancesInfo, checksum));
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum)));
#else
quint16 checksum = qChecksum(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum));
#endif
return checksum;
}
qint64 SingleApplicationPrivate::primaryPid() {
qint64 pid;
memory->lock();
InstancesInfo* inst = static_cast<InstancesInfo*>(memory->data());
pid = inst->primaryPid;
memory->unlock();
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
qint64 pid = inst->primaryPid;
memory_->unlock();
return pid;
}
QString SingleApplicationPrivate::primaryUser() {
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
QByteArray username = inst->primaryUser;
memory_->unlock();
return QString::fromUtf8(username);
}
/**
* @brief Executed when a connection has been made to the LocalServer
*/
void SingleApplicationPrivate::slotConnectionEstablished() {
QLocalSocket *nextConnSocket = server->nextPendingConnection();
connectionMap.insert(nextConnSocket, ConnectionInfo());
QLocalSocket *nextConnSocket = server_->nextPendingConnection();
connectionMap_.insert(nextConnSocket, ConnectionInfo());
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose,
[nextConnSocket, this]() {
auto &info = connectionMap[nextConnSocket];
Q_EMIT this->slotClientConnectionClosed(nextConnSocket, info.instanceId);
}
);
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, [nextConnSocket, this]() {
auto &info = connectionMap_[nextConnSocket];
Q_EMIT this->slotClientConnectionClosed(nextConnSocket, info.instanceId);
});
QObject::connect(nextConnSocket, &QLocalSocket::disconnected,
[nextConnSocket, this](){
connectionMap.remove(nextConnSocket);
nextConnSocket->deleteLater();
}
);
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, [nextConnSocket, this]() {
connectionMap_.remove(nextConnSocket);
nextConnSocket->deleteLater();
});
QObject::connect(nextConnSocket, &QLocalSocket::readyRead,
[nextConnSocket, this]() {
auto &info = connectionMap[nextConnSocket];
switch(info.stage) {
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, [nextConnSocket, this]() {
auto &info = connectionMap_[nextConnSocket];
switch (info.stage) {
case StageHeader:
readInitMessageHeader(nextConnSocket);
break;
@@ -294,15 +350,14 @@ void SingleApplicationPrivate::slotConnectionEstablished() {
break;
default:
break;
};
}
);
};
});
}
void SingleApplicationPrivate::readInitMessageHeader(QLocalSocket *sock) {
if (!connectionMap.contains(sock)) {
if (!connectionMap_.contains(sock)) {
return;
}
@@ -311,13 +366,12 @@ void SingleApplicationPrivate::readInitMessageHeader(QLocalSocket *sock) {
}
QDataStream headerStream(sock);
headerStream.setVersion(QDataStream::Qt_5_6);
headerStream.setVersion(QDataStream::Qt_5_8);
// Read the header to know the message length
quint64 msgLen = 0;
headerStream >> msgLen;
ConnectionInfo &info = connectionMap[sock];
ConnectionInfo &info = connectionMap_[sock];
info.stage = StageBody;
info.msgLen = msgLen;
@@ -331,11 +385,11 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
Q_Q(SingleApplication);
if (!connectionMap.contains(sock)) {
if (!connectionMap_.contains(sock)) {
return;
}
ConnectionInfo &info = connectionMap[sock];
ConnectionInfo &info = connectionMap_[sock];
if (sock->bytesAvailable() < static_cast<qint64>(info.msgLen)) {
return;
}
@@ -343,8 +397,7 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
// Read the message body
QByteArray msgBytes = sock->read(info.msgLen);
QDataStream readStream(msgBytes);
readStream.setVersion(QDataStream::Qt_5_6);
readStream.setVersion(QDataStream::Qt_5_8);
// server name
QByteArray latin1Name;
@@ -354,7 +407,7 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
ConnectionType connectionType = InvalidConnection;
quint8 connTypeVal = InvalidConnection;
readStream >> connTypeVal;
connectionType = static_cast <ConnectionType>(connTypeVal);
connectionType = static_cast<ConnectionType>(connTypeVal);
// instance id
quint32 instanceId = 0;
@@ -364,9 +417,13 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
quint16 msgChecksum = 0;
readStream >> msgChecksum;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast<quint32>(msgBytes.length() - sizeof(quint16))));
#else
const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast<quint32>(msgBytes.length() - sizeof(quint16)));
#endif
bool isValid = readStream.status() == QDataStream::Ok && QLatin1String(latin1Name) == blockServerName && msgChecksum == actualChecksum;
bool isValid = readStream.status() == QDataStream::Ok && QLatin1String(latin1Name) == blockServerName_ && msgChecksum == actualChecksum;
if (!isValid) {
sock->close();
@@ -376,7 +433,7 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
info.instanceId = instanceId;
info.stage = StageConnected;
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options & SingleApplication::Mode::SecondaryNotification)) {
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleApplication::Mode::SecondaryNotification)) {
Q_EMIT q->instanceStarted();
}
@@ -395,7 +452,19 @@ void SingleApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const
void SingleApplicationPrivate::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
if (closedSocket->bytesAvailable() > 0)
if (closedSocket->bytesAvailable() > 0) {
Q_EMIT slotDataAvailable(closedSocket, instanceId);
}
}
void SingleApplicationPrivate::randomSleep() {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QThread::msleep(QRandomGenerator::global()->bounded(8u, 18u));
#else
qsrand(QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max());
QThread::msleep(8 + static_cast<unsigned long>(static_cast<float>(qrand()) / RAND_MAX * 10));
#endif
}

View File

@@ -1,6 +1,6 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2016
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -36,6 +36,7 @@
#include <QtGlobal>
#include <QObject>
#include <QString>
#include <QMap>
#include "singleapplication.h"
@@ -48,6 +49,7 @@ struct InstancesInfo {
bool primary;
quint32 secondary;
qint64 primaryPid;
char primaryUser[128];
quint16 checksum;
};
@@ -60,6 +62,7 @@ struct ConnectionInfo {
class SingleApplicationPrivate : public QObject {
Q_OBJECT
public:
enum ConnectionType : quint8 {
InvalidConnection = 0,
@@ -74,27 +77,30 @@ class SingleApplicationPrivate : public QObject {
};
Q_DECLARE_PUBLIC(SingleApplication)
explicit SingleApplicationPrivate(SingleApplication *_q_ptr);
explicit SingleApplicationPrivate(SingleApplication *ptr);
~SingleApplicationPrivate() override;
QString getUsername();
void genBlockServerName();
void initializeMemoryBlock();
void startPrimary();
void startSecondary();
void connectToPrimary(const int msecs, const ConnectionType connectionType);
bool connectToPrimary(const int msecs, const ConnectionType connectionType);
quint16 blockChecksum();
qint64 primaryPid();
QString primaryUser();
void readInitMessageHeader(QLocalSocket *socket);
void readInitMessageBody(QLocalSocket *socket);
void randomSleep();
SingleApplication *q_ptr;
QSharedMemory *memory;
QLocalSocket *socket;
QLocalServer *server;
quint32 instanceNumber;
QString blockServerName;
SingleApplication::Options options;
QMap<QLocalSocket*, ConnectionInfo> connectionMap;
QSharedMemory *memory_;
QLocalSocket *socket_;
QLocalServer *server_;
quint32 instanceNumber_;
QString blockServerName_;
SingleApplication::Options options_;
QMap<QLocalSocket*, ConnectionInfo> connectionMap_;
public slots:
void slotConnectionEstablished();

View File

@@ -1,6 +1,6 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2018
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -42,106 +42,124 @@
#include <QByteArray>
#include <QElapsedTimer>
#include <QtDebug>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#else
# include <QDateTime>
#endif
#include "singlecoreapplication.h"
#include "singlecoreapplication_p.h"
/**
* @brief Constructor. Checks and fires up LocalServer or closes the program
* if another instance already exists
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
* @param argc
* @param argv
* @param {bool} allowSecondaryInstances
* @param allowSecondary Whether to enable secondary instance support
* @param options Optional flags to toggle specific behaviour
* @param timeout Maximum time blocking functions are allowed during app load
*/
SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], bool allowSecondary, Options options, int timeout)
: app_t(argc, argv), d_ptr(new SingleCoreApplicationPrivate(this)) {
: app_t(argc, argv),
d_ptr(new SingleCoreApplicationPrivate(this)) {
Q_D(SingleCoreApplication);
// Store the current mode of the program
d->options = options;
d->options_ = options;
// Generating an application ID used for identifying the shared memory block and QLocalServer
d->genBlockServerName();
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
d->randomSleep();
#ifdef Q_OS_UNIX
// By explicitly attaching it and then deleting it we make sure that the
// memory is deleted even after the process has crashed on Unix.
d->memory = new QSharedMemory(d->blockServerName);
d->memory->attach();
delete d->memory;
// By explicitly attaching it and then deleting it we make sure that the memory is deleted even after the process has crashed on Unix.
d->memory_ = new QSharedMemory(d->blockServerName_);
d->memory_->attach();
delete d->memory_;
#endif
// Guarantee thread safe behaviour with a shared memory block.
d->memory = new QSharedMemory(d->blockServerName);
d->memory_ = new QSharedMemory(d->blockServerName_);
// Create a shared memory block
if (d->memory->create(sizeof(InstancesInfo))) {
if (d->memory_->create(sizeof(InstancesInfo))) {
// Initialize the shared memory block
d->memory->lock();
if (!d->memory_->lock()) {
qCritical() << "SingleCoreApplication: Unable to lock memory block after create.";
abortSafely();
}
d->initializeMemoryBlock();
d->memory->unlock();
}
else {
// Attempt to attach to the memory segment
if (!d->memory->attach()) {
qCritical() << "SingleCoreApplication: Unable to attach to shared memory block.";
qCritical() << d->memory->errorString();
delete d;
::exit(EXIT_FAILURE);
if (d->memory_->error() == QSharedMemory::AlreadyExists) {
// Attempt to attach to the memory segment
if (!d->memory_->attach()) {
qCritical() << "SingleCoreApplication: Unable to attach to shared memory block.";
abortSafely();
}
if (!d->memory_->lock()) {
qCritical() << "SingleCoreApplication: Unable to lock memory block after attach.";
abortSafely();
}
}
else {
qCritical() << "SingleCoreApplication: Unable to create block.";
abortSafely();
}
}
InstancesInfo* inst = static_cast<InstancesInfo*>(d->memory->data());
InstancesInfo *inst = static_cast<InstancesInfo*>(d->memory_->data());
QElapsedTimer time;
time.start();
// Make sure the shared memory block is initialised and in consistent state
while (true) {
d->memory->lock();
if(d->blockChecksum() == inst->checksum) break;
forever {
// If the shared memory block's checksum is valid continue
if (d->blockChecksum() == inst->checksum) break;
// If more than 5s have elapsed, assume the primary instance crashed and assume it's position
if (time.elapsed() > 5000) {
qWarning() << "SingleCoreApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure.";
d->initializeMemoryBlock();
}
d->memory->unlock();
// Random sleep here limits the probability of a collision between two racing apps
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QThread::sleep(QRandomGenerator::global()->bounded(8u, 18u));
#else
qsrand(QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max());
QThread::sleep(8 + static_cast<unsigned long>(static_cast <float>(qrand()) / RAND_MAX * 10));
#endif
// Otherwise wait for a random period and try again.
// The random sleep here limits the probability of a collision between two racing apps and allows the app to initialise faster
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory for random wait.";
qDebug() << d->memory_->errorString();
}
d->randomSleep();
if (!d->memory_->lock()) {
qCritical() << "SingleCoreApplication: Unable to lock memory after random wait.";
abortSafely();
}
}
if (inst->primary == false) {
d->startPrimary();
d->memory->unlock();
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory after primary start.";
qDebug() << d->memory_->errorString();
}
return;
}
// Check if another instance can be started
if (allowSecondary) {
inst->secondary += 1;
inst->checksum = d->blockChecksum();
d->instanceNumber = inst->secondary;
d->startSecondary();
if(d->options & Mode::SecondaryNotification) {
if (d->options_ & Mode::SecondaryNotification) {
d->connectToPrimary(timeout, SingleCoreApplicationPrivate::SecondaryInstance);
}
d->memory->unlock();
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory after secondary start.";
qDebug() << d->memory_->errorString();
}
return;
}
d->memory->unlock();
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory at end of execution.";
qDebug() << d->memory_->errorString();
}
d->connectToPrimary(timeout, SingleCoreApplicationPrivate::NewInstance);
@@ -151,47 +169,101 @@ SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], bool allow
}
/**
* @brief Destructor
*/
SingleCoreApplication::~SingleCoreApplication() {
Q_D(SingleCoreApplication);
delete d;
}
/**
* Checks if the current application instance is primary.
* @return Returns true if the instance is primary, false otherwise.
*/
bool SingleCoreApplication::isPrimary() {
Q_D(SingleCoreApplication);
return d->server != nullptr;
return d->server_ != nullptr;
}
/**
* Checks if the current application instance is secondary.
* @return Returns true if the instance is secondary, false otherwise.
*/
bool SingleCoreApplication::isSecondary() {
Q_D(SingleCoreApplication);
return d->server == nullptr;
return d->server_ == nullptr;
}
/**
* Allows you to identify an instance by returning unique consecutive instance ids.
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
* @return Returns a unique instance id.
*/
quint32 SingleCoreApplication::instanceId() {
Q_D(SingleCoreApplication);
return d->instanceNumber;
return d->instanceNumber_;
}
/**
* Returns the OS PID (Process Identifier) of the process running the primary instance.
* Especially useful when SingleCoreApplication is coupled with OS. specific APIs.
* @return Returns the primary instance PID.
*/
qint64 SingleCoreApplication::primaryPid() {
Q_D(SingleCoreApplication);
return d->primaryPid();
}
/**
* Returns the username the primary instance is running as.
* @return Returns the username the primary instance is running as.
*/
QString SingleCoreApplication::primaryUser() {
Q_D(SingleCoreApplication);
return d->primaryUser();
}
/**
* Returns the username the current instance is running as.
* @return Returns the username the current instance is running as.
*/
QString SingleCoreApplication::currentUser() {
Q_D(SingleCoreApplication);
return d->getUsername();
}
/**
* Sends message to the Primary Instance.
* @param message The message to send.
* @param timeout the maximum timeout in milliseconds for blocking functions.
* @return true if the message was sent successfuly, false otherwise.
*/
bool SingleCoreApplication::sendMessage(QByteArray message, int timeout) {
Q_D(SingleCoreApplication);
// Nobody to connect to
if(isPrimary()) return false;
if (isPrimary()) return false;
// Make sure the socket is connected
d->connectToPrimary(timeout, SingleCoreApplicationPrivate::Reconnect);
if (!d->connectToPrimary(timeout, SingleCoreApplicationPrivate::Reconnect))
return false;
d->socket->write(message);
bool dataWritten = d->socket->waitForBytesWritten(timeout);
d->socket->flush();
d->socket_->write(message);
const bool dataWritten = d->socket_->waitForBytesWritten(timeout);
d->socket_->flush();
return dataWritten;
}
/**
* Cleans up the shared memory block and exits with a failure.
* This function halts program execution.
*/
void SingleCoreApplication::abortSafely() {
Q_D(SingleCoreApplication);
qCritical() << "SingleCoreApplication: " << d->memory_->error() << d->memory_->errorString();
delete d;
::exit(EXIT_FAILURE);
}

View File

@@ -1,6 +1,6 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2018
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -42,8 +42,7 @@
class SingleCoreApplicationPrivate;
/**
* @brief The SingleCoreApplication class handles multiple instances of the same
* Application
* @brief The SingleCoreApplication class handles multipe instances of the same Application
* @see QCoreApplication
*/
class SingleCoreApplication : public QCoreApplication {
@@ -51,7 +50,7 @@ class SingleCoreApplication : public QCoreApplication {
typedef QCoreApplication app_t;
public:
public:
/**
* @brief Mode of operation of SingleCoreApplication.
* Whether the block should be user-wide or system-wide and whether the
@@ -63,11 +62,11 @@ public:
* @enum
*/
enum Mode {
User = 1 << 0,
System = 1 << 1,
SecondaryNotification = 1 << 2,
ExcludeAppVersion = 1 << 3,
ExcludeAppPath = 1 << 4
User = 1 << 0,
System = 1 << 1,
SecondaryNotification = 1 << 2,
ExcludeAppVersion = 1 << 3,
ExcludeAppPath = 1 << 4
};
Q_DECLARE_FLAGS(Options, Mode)
@@ -89,9 +88,8 @@ public:
* operations. It does not guarantee that the SingleCoreApplication
* initialisation will be completed in given time, though is a good hint.
* Usually 4*timeout would be the worst case (fail) scenario.
* @see See the corresponding QAPPLICATION_CLASS constructor for reference
*/
explicit SingleCoreApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000 );
explicit SingleCoreApplication(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
~SingleCoreApplication() override;
/**
@@ -118,6 +116,18 @@ public:
*/
qint64 primaryPid();
/**
* @brief Returns the username of the user running the primary instance
* @returns {QString}
*/
QString primaryUser();
/**
* @brief Returns the username of the current user
* @returns {QString}
*/
QString currentUser();
/**
* @brief Sends a message to the primary instance. Returns true on success.
* @param {int} timeout - Timeout for connecting
@@ -125,17 +135,18 @@ public:
* @note sendMessage() will return false if invoked from the primary
* instance.
*/
bool sendMessage( QByteArray message, int timeout = 1000 );
bool sendMessage(QByteArray message, int timeout = 1000);
signals:
void instanceStarted();
void receivedMessage( quint32 instanceId, QByteArray message );
void receivedMessage(quint32 instanceId, QByteArray message);
private:
private:
SingleCoreApplicationPrivate *d_ptr;
Q_DECLARE_PRIVATE(SingleCoreApplication)
void abortSafely();
};
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleCoreApplication::Options)
#endif // SINGLECOREAPPLICATION_H
#endif // SINGLECOREAPPLICATION_H

View File

@@ -1,6 +1,6 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2018
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -44,6 +44,8 @@
# include <pwd.h>
#endif
#include <QObject>
#include <QThread>
#include <QIODevice>
#include <QSharedMemory>
#include <QByteArray>
@@ -52,6 +54,12 @@
#include <QLocalServer>
#include <QLocalSocket>
#include <QDir>
#include <QElapsedTimer>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#else
# include <QDateTime>
#endif
#include "singlecoreapplication.h"
#include "singlecoreapplication_p.h"
@@ -61,33 +69,73 @@
# include <lmcons.h>
#endif
SingleCoreApplicationPrivate::SingleCoreApplicationPrivate(SingleCoreApplication *_q_ptr)
: q_ptr(_q_ptr),
memory(nullptr),
socket(nullptr),
server(nullptr),
instanceNumber(-1)
{}
SingleCoreApplicationPrivate::SingleCoreApplicationPrivate(SingleCoreApplication *ptr)
: q_ptr(ptr),
memory_(nullptr),
socket_(nullptr),
server_(nullptr),
instanceNumber_(-1) {}
SingleCoreApplicationPrivate::~SingleCoreApplicationPrivate() {
if (socket != nullptr) {
socket->close();
delete socket;
if (socket_ != nullptr) {
socket_->close();
delete socket_;
socket_ = nullptr;
}
memory->lock();
InstancesInfo* inst = static_cast<InstancesInfo*>(memory->data());
if (server != nullptr) {
server->close();
delete server;
inst->primary = false;
inst->primaryPid = -1;
inst->checksum = blockChecksum();
}
memory->unlock();
if (memory_ != nullptr) {
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
if (server_ != nullptr) {
server_->close();
delete server_;
inst->primary = false;
inst->primaryPid = -1;
inst->primaryUser[0] = '\0';
inst->checksum = blockChecksum();
}
memory_->unlock();
delete memory;
delete memory_;
memory_ = nullptr;
}
}
QString SingleCoreApplicationPrivate::getUsername() {
#ifdef Q_OS_UNIX
QString username;
#if defined(HAVE_GETEUID) && defined(HAVE_GETPWUID)
struct passwd *pw = getpwuid(geteuid());
if (pw) {
username = QString::fromLocal8Bit(pw->pw_name);
}
#endif
if (username.isEmpty()) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
username = qEnvironmentVariable("USER");
#else
username = QString::fromLocal8Bit(qgetenv("USER"));
#endif
}
return username;
#endif
#ifdef Q_OS_WIN
wchar_t username[UNLEN + 1];
// Specifies size of the buffer on input
DWORD usernameLength = UNLEN + 1;
if (GetUserNameW(username, &usernameLength)) {
return QString::fromWCharArray(username);
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
return qEnvironmentVariable("USERNAME");
#else
return QString::fromLocal8Bit(qgetenv("USERNAME"));
#endif
#endif
}
@@ -99,11 +147,11 @@ void SingleCoreApplicationPrivate::genBlockServerName() {
appData.addData(SingleCoreApplication::app_t::organizationName().toUtf8());
appData.addData(SingleCoreApplication::app_t::organizationDomain().toUtf8());
if (!(options & SingleCoreApplication::Mode::ExcludeAppVersion)) {
if (!(options_ & SingleCoreApplication::Mode::ExcludeAppVersion)) {
appData.addData(SingleCoreApplication::app_t::applicationVersion().toUtf8());
}
if (!(options & SingleCoreApplication::Mode::ExcludeAppPath)) {
if (!(options_ & SingleCoreApplication::Mode::ExcludeAppPath)) {
#ifdef Q_OS_WIN
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toLower().toUtf8());
#else
@@ -112,42 +160,22 @@ void SingleCoreApplicationPrivate::genBlockServerName() {
}
// User level block requires a user specific data in the hash
if (options & SingleCoreApplication::Mode::User) {
#ifdef Q_OS_UNIX
QByteArray username;
#if defined(HAVE_GETEUID) && defined(HAVE_GETPWUID)
struct passwd *pw = getpwuid(geteuid());
if (pw) {
username = pw->pw_name;
}
#endif
if (username.isEmpty()) username = qgetenv("USER");
appData.addData(username);
#endif
#ifdef Q_OS_WIN
wchar_t username [ UNLEN + 1 ];
// Specifies size of the buffer on input
DWORD usernameLength = UNLEN + 1;
if (GetUserNameW(username, &usernameLength)) {
appData.addData(QString::fromWCharArray(username).toUtf8());
}
else {
appData.addData(qgetenv("USERNAME"));
}
#endif
if (options_ & SingleCoreApplication::Mode::User) {
appData.addData(getUsername().toUtf8());
}
// Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with server naming requirements.
blockServerName = appData.result().toBase64().replace("/", "_");
blockServerName_ = appData.result().toBase64().replace("/", "_");
}
void SingleCoreApplicationPrivate::initializeMemoryBlock() {
InstancesInfo* inst = static_cast<InstancesInfo*>(memory->data());
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
inst->primary = false;
inst->secondary = 0;
inst->primaryPid = -1;
inst->primaryUser[0] = '\0';
inst->checksum = blockChecksum();
}
@@ -156,133 +184,161 @@ void SingleCoreApplicationPrivate::startPrimary() {
Q_Q(SingleCoreApplication);
// Successful creation means that no main process exists
// So we start a QLocalServer to listen for connections
QLocalServer::removeServer(blockServerName);
server = new QLocalServer();
// Restrict access to the socket according to the
// SingleCoreApplication::Mode::User flag on User level or no restrictions
if (options & SingleCoreApplication::Mode::User) {
server->setSocketOptions(QLocalServer::UserAccessOption);
}
else {
server->setSocketOptions(QLocalServer::WorldAccessOption);
}
server->listen(blockServerName);
QObject::connect(server, &QLocalServer::newConnection, this, &SingleCoreApplicationPrivate::slotConnectionEstablished);
// Reset the number of connections
InstancesInfo* inst = static_cast <InstancesInfo*>(memory->data());
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
inst->primary = true;
inst->primaryPid = q->applicationPid();
qstrncpy(inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser));
inst->checksum = blockChecksum();
instanceNumber_ = 0;
// Successful creation means that no main process exists
// So we start a QLocalServer to listen for connections
QLocalServer::removeServer(blockServerName_);
server_ = new QLocalServer();
instanceNumber = 0;
// Restrict access to the socket according to the SingleCoreApplication::Mode::User flag on User level or no restrictions
if (options_ & SingleCoreApplication::Mode::User) {
server_->setSocketOptions(QLocalServer::UserAccessOption);
}
else {
server_->setSocketOptions(QLocalServer::WorldAccessOption);
}
server_->listen(blockServerName_);
QObject::connect(server_, &QLocalServer::newConnection, this, &SingleCoreApplicationPrivate::slotConnectionEstablished);
}
void SingleCoreApplicationPrivate::startSecondary() {}
void SingleCoreApplicationPrivate::startSecondary() {
void SingleCoreApplicationPrivate::connectToPrimary(const int msecs, const ConnectionType connectionType) {
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
inst->secondary += 1;
inst->checksum = blockChecksum();
instanceNumber_ = inst->secondary;
}
bool SingleCoreApplicationPrivate::connectToPrimary(int timeout, ConnectionType connectionType) {
QElapsedTimer time;
time.start();
// Connect to the Local Server of the Primary Instance if not already connected.
if (socket == nullptr) {
socket = new QLocalSocket();
if (socket_ == nullptr) {
socket_ = new QLocalSocket();
}
// If already connected - we are done;
if (socket->state() == QLocalSocket::ConnectedState)
return;
if (socket_->state() == QLocalSocket::ConnectedState) return true;
// If not connect
if (socket->state() == QLocalSocket::UnconnectedState ||
socket->state() == QLocalSocket::ClosingState) {
socket->connectToServer(blockServerName);
}
if (socket_->state() != QLocalSocket::ConnectedState) {
// Wait for being connected
if (socket->state() == QLocalSocket::ConnectingState) {
socket->waitForConnected(msecs);
forever {
randomSleep();
if (socket_->state() != QLocalSocket::ConnectingState)
socket_->connectToServer(blockServerName_);
if (socket_->state() == QLocalSocket::ConnectingState) {
socket_->waitForConnected(timeout - time.elapsed());
}
// If connected break out of the loop
if (socket_->state() == QLocalSocket::ConnectedState) break;
// If elapsed time since start is longer than the method timeout return
if (time.elapsed() >= timeout) return false;
}
}
// Initialisation message according to the SingleCoreApplication protocol
if (socket->state() == QLocalSocket::ConnectedState) {
// Notify the parent that a new instance had been started;
QByteArray initMsg;
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
QByteArray initMsg;
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
writeStream.setVersion(QDataStream::Qt_5_8);
writeStream.setVersion(QDataStream::Qt_5_6);
writeStream << blockServerName_.toLatin1();
writeStream << static_cast<quint8>(connectionType);
writeStream << instanceNumber_;
writeStream << blockServerName.toLatin1();
writeStream << static_cast<quint8>(connectionType);
writeStream << instanceNumber;
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
writeStream << checksum;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
#else
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
#endif
// The header indicates the message length that follows
QByteArray header;
QDataStream headerStream(&header, QIODevice::WriteOnly);
writeStream << checksum;
headerStream.setVersion(QDataStream::Qt_5_6);
// The header indicates the message length that follows
QByteArray header;
QDataStream headerStream(&header, QIODevice::WriteOnly);
headerStream.setVersion(QDataStream::Qt_5_8);
headerStream << static_cast<quint64>(initMsg.length());
headerStream << static_cast <quint64>(initMsg.length());
socket_->write(header);
socket_->write(initMsg);
bool result = socket_->waitForBytesWritten(timeout - time.elapsed());
socket_->flush();
socket->write(header);
socket->write(initMsg);
socket->flush();
socket->waitForBytesWritten(msecs);
}
return result;
}
quint16 SingleCoreApplicationPrivate::blockChecksum() {
return qChecksum(static_cast <const char*> (memory->data()), offsetof(InstancesInfo, checksum));
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum)));
#else
quint16 checksum = qChecksum(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum));
#endif
return checksum;
}
qint64 SingleCoreApplicationPrivate::primaryPid() {
qint64 pid;
memory->lock();
InstancesInfo* inst = static_cast<InstancesInfo*>(memory->data());
pid = inst->primaryPid;
memory->unlock();
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
qint64 pid = inst->primaryPid;
memory_->unlock();
return pid;
}
QString SingleCoreApplicationPrivate::primaryUser() {
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
QByteArray username = inst->primaryUser;
memory_->unlock();
return QString::fromUtf8(username);
}
/**
* @brief Executed when a connection has been made to the LocalServer
*/
void SingleCoreApplicationPrivate::slotConnectionEstablished() {
QLocalSocket *nextConnSocket = server->nextPendingConnection();
connectionMap.insert(nextConnSocket, ConnectionInfo());
QLocalSocket *nextConnSocket = server_->nextPendingConnection();
connectionMap_.insert(nextConnSocket, ConnectionInfo());
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose,
[nextConnSocket, this]() {
auto &info = connectionMap[nextConnSocket];
Q_EMIT this->slotClientConnectionClosed(nextConnSocket, info.instanceId);
}
);
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, [nextConnSocket, this]() {
auto &info = connectionMap_[nextConnSocket];
Q_EMIT this->slotClientConnectionClosed(nextConnSocket, info.instanceId);
});
QObject::connect(nextConnSocket, &QLocalSocket::disconnected,
[nextConnSocket, this](){
connectionMap.remove(nextConnSocket);
nextConnSocket->deleteLater();
}
);
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, [nextConnSocket, this]() {
connectionMap_.remove(nextConnSocket);
nextConnSocket->deleteLater();
});
QObject::connect(nextConnSocket, &QLocalSocket::readyRead,
[nextConnSocket, this]() {
auto &info = connectionMap[nextConnSocket];
switch(info.stage) {
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, [nextConnSocket, this]() {
auto &info = connectionMap_[nextConnSocket];
switch (info.stage) {
case StageHeader:
readInitMessageHeader(nextConnSocket);
break;
@@ -294,15 +350,14 @@ void SingleCoreApplicationPrivate::slotConnectionEstablished() {
break;
default:
break;
};
}
);
};
});
}
void SingleCoreApplicationPrivate::readInitMessageHeader(QLocalSocket *sock) {
if (!connectionMap.contains(sock)) {
if (!connectionMap_.contains(sock)) {
return;
}
@@ -311,13 +366,12 @@ void SingleCoreApplicationPrivate::readInitMessageHeader(QLocalSocket *sock) {
}
QDataStream headerStream(sock);
headerStream.setVersion(QDataStream::Qt_5_6);
headerStream.setVersion(QDataStream::Qt_5_8);
// Read the header to know the message length
quint64 msgLen = 0;
headerStream >> msgLen;
ConnectionInfo &info = connectionMap[sock];
ConnectionInfo &info = connectionMap_[sock];
info.stage = StageBody;
info.msgLen = msgLen;
@@ -331,11 +385,11 @@ void SingleCoreApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
Q_Q(SingleCoreApplication);
if (!connectionMap.contains(sock)) {
if (!connectionMap_.contains(sock)) {
return;
}
ConnectionInfo &info = connectionMap[sock];
ConnectionInfo &info = connectionMap_[sock];
if (sock->bytesAvailable() < static_cast<qint64>(info.msgLen)) {
return;
}
@@ -343,8 +397,7 @@ void SingleCoreApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
// Read the message body
QByteArray msgBytes = sock->read(info.msgLen);
QDataStream readStream(msgBytes);
readStream.setVersion(QDataStream::Qt_5_6);
readStream.setVersion(QDataStream::Qt_5_8);
// server name
QByteArray latin1Name;
@@ -354,7 +407,7 @@ void SingleCoreApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
ConnectionType connectionType = InvalidConnection;
quint8 connTypeVal = InvalidConnection;
readStream >> connTypeVal;
connectionType = static_cast <ConnectionType>(connTypeVal);
connectionType = static_cast<ConnectionType>(connTypeVal);
// instance id
quint32 instanceId = 0;
@@ -364,9 +417,13 @@ void SingleCoreApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
quint16 msgChecksum = 0;
readStream >> msgChecksum;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast<quint32>(msgBytes.length() - sizeof(quint16))));
#else
const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast<quint32>(msgBytes.length() - sizeof(quint16)));
#endif
bool isValid = readStream.status() == QDataStream::Ok && QLatin1String(latin1Name) == blockServerName && msgChecksum == actualChecksum;
bool isValid = readStream.status() == QDataStream::Ok && QLatin1String(latin1Name) == blockServerName_ && msgChecksum == actualChecksum;
if (!isValid) {
sock->close();
@@ -376,7 +433,7 @@ void SingleCoreApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
info.instanceId = instanceId;
info.stage = StageConnected;
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options & SingleCoreApplication::Mode::SecondaryNotification)) {
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleCoreApplication::Mode::SecondaryNotification)) {
Q_EMIT q->instanceStarted();
}
@@ -395,7 +452,19 @@ void SingleCoreApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, c
void SingleCoreApplicationPrivate::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
if (closedSocket->bytesAvailable() > 0)
if (closedSocket->bytesAvailable() > 0) {
Q_EMIT slotDataAvailable(closedSocket, instanceId);
}
}
void SingleCoreApplicationPrivate::randomSleep() {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QThread::msleep(QRandomGenerator::global()->bounded(8u, 18u));
#else
qsrand(QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max());
QThread::msleep(8 + static_cast<unsigned long>(static_cast<float>(qrand()) / RAND_MAX * 10));
#endif
}

View File

@@ -1,6 +1,6 @@
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2016
// Copyright (c) Itay Grudev 2015 - 2020
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -36,6 +36,7 @@
#include <QtGlobal>
#include <QObject>
#include <QString>
#include <QMap>
#include "singlecoreapplication.h"
@@ -48,6 +49,7 @@ struct InstancesInfo {
bool primary;
quint32 secondary;
qint64 primaryPid;
char primaryUser[128];
quint16 checksum;
};
@@ -60,6 +62,7 @@ struct ConnectionInfo {
class SingleCoreApplicationPrivate : public QObject {
Q_OBJECT
public:
enum ConnectionType : quint8 {
InvalidConnection = 0,
@@ -74,27 +77,30 @@ class SingleCoreApplicationPrivate : public QObject {
};
Q_DECLARE_PUBLIC(SingleCoreApplication)
explicit SingleCoreApplicationPrivate(SingleCoreApplication *_q_ptr);
explicit SingleCoreApplicationPrivate(SingleCoreApplication *ptr);
~SingleCoreApplicationPrivate() override;
QString getUsername();
void genBlockServerName();
void initializeMemoryBlock();
void startPrimary();
void startSecondary();
void connectToPrimary(const int msecs, const ConnectionType connectionType);
bool connectToPrimary(const int msecs, const ConnectionType connectionType);
quint16 blockChecksum();
qint64 primaryPid();
QString primaryUser();
void readInitMessageHeader(QLocalSocket *socket);
void readInitMessageBody(QLocalSocket *socket);
void randomSleep();
SingleCoreApplication *q_ptr;
QSharedMemory *memory;
QLocalSocket *socket;
QLocalServer *server;
quint32 instanceNumber;
QString blockServerName;
SingleCoreApplication::Options options;
QMap<QLocalSocket*, ConnectionInfo> connectionMap;
QSharedMemory *memory_;
QLocalSocket *socket_;
QLocalServer *server_;
quint32 instanceNumber_;
QString blockServerName_;
SingleCoreApplication::Options options_;
QMap<QLocalSocket*, ConnectionInfo> connectionMap_;
public slots:
void slotConnectionEstablished();

View File

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

View File

@@ -13,7 +13,9 @@ include(cmake/OptionalSource.cmake)
include(cmake/ParseArguments.cmake)
include(cmake/Rpm.cmake)
include(cmake/Deb.cmake)
include(cmake/Dmg.cmake)
if(APPLE)
include(cmake/Dmg.cmake)
endif()
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
@@ -27,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
@@ -91,6 +93,7 @@ find_package(Backtrace QUIET)
if(Backtrace_FOUND)
set(HAVE_BACKTRACE ON)
endif()
find_package(Iconv QUIET)
find_package(GnuTLS REQUIRED)
find_package(Protobuf REQUIRED)
if (NOT Protobuf_PROTOC_EXECUTABLE)
@@ -126,10 +129,37 @@ pkg_check_modules(LIBPULSE libpulse)
pkg_check_modules(CHROMAPRINT libchromaprint)
pkg_check_modules(LIBGPOD libgpod-1.0>=0.7.92)
pkg_check_modules(LIBMTP libmtp>=1.0)
pkg_check_modules(GDK_PIXBUF gdk-pixbuf-2.0)
find_package(Gettext)
find_package(FFTW3)
option(WITH_QT6 "Use Qt 6" OFF)
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)
if(WITH_QT6)
set(BUILD_WITH_QT6 ON)
endif()
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)
if(X11_FOUND)
list(APPEND QT_COMPONENTS X11Extras)
@@ -137,78 +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(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()
else()
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()
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)
@@ -261,7 +248,7 @@ if(APPLE)
endif(APPLE)
if(NOT SPARKLE AND (APPLE OR WIN32))
if(WITH_QT6)
if(BUILD_WITH_QT6)
pkg_check_modules(QTSPARKLE qtsparkle-qt6)
else()
pkg_check_modules(QTSPARKLE qtsparkle-qt5)
@@ -335,6 +322,7 @@ optional_component(GIO ON "Devices: GIO device backend"
optional_component(LIBGPOD ON "Devices: iPod classic support"
DEPENDS "libgpod" LIBGPOD_FOUND
DEPENDS "gdk-pixbuf" GDK_PIXBUF_FOUND
)
optional_component(LIBMTP ON "Devices: MTP support"
@@ -346,7 +334,7 @@ optional_component(SPARKLE ON "Sparkle integration"
DEPENDS "Sparkle" SPARKLE
)
if(WITH_QT6)
if(BUILD_WITH_QT6)
optional_component(TRANSLATIONS ON "Translations"
DEPENDS "gettext" GETTEXT_FOUND
DEPENDS "Qt6LinguistTools" Qt6LinguistTools_FOUND
@@ -362,6 +350,7 @@ option(INSTALL_TRANSLATIONS "Install translations" OFF)
optional_component(SUBSONIC ON "Subsonic support")
optional_component(TIDAL ON "Tidal support")
optional_component(QOBUZ ON "Qobuz support")
optional_component(MOODBAR ON "Moodbar"
DEPENDS "fftw3" FFTW3_FOUND
@@ -386,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>

102
Changelog
View File

@@ -2,9 +2,109 @@ 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:
* Fixed broken transition to next song for CUE files with certain audio formats (regression since version 0.6.13).
* Fixed all collection divider keys showing on top with some language collate settings (regression in version 0.8.1).
* Fixed SQL querying songs by song ID when song ID is a string.
* Fixed saving album covers for LMS Subsonic servers.
* Fixed reading song creation dates with LMS Subsonic servers.
* Fixed saving initial settings.
* Removed use of HTML in system tray icon tooltip for all desktop environments instead of just KDE and Cinnamon.
* (Windows) Ignore "IDirectSoundBuffer_GetStatus The operation completed successfully" false error when switching device while playing.
0.8.1:
Bugfixes:
* Fixed engine selection in backend settings with Qt 6.
* Fixed pixelated playlist source icon for currently playing song.
* Fixed crash when deleting queued songs from playlist.
* Fixed situations where songs could disappear or be shown multiple times with certain collection groupings.
* Fixed initial sizes of playlist header columns.
* Fixed Strawberry preventing logout.
* Fixed incorrectly splitting of basename for moodbar and transcoding for filenames with several dots.
* Fixed certain cases where "playing now" for scrobbler were sent twice.
* Fixed album cover loaded twice for certain songs causing slugglish playing widget.
* Fixed playing widget to draw text after album cover is fully shown.
* Fixed crash when trying to copy a closed playlist to a device.
* Fixed incorrect song source for CUE songs when added through the collection watcher.
* Disable use of HTML in system tray tooltip on Cinnamon too.
* Remove problematic '&' character from OSD messages.
* (macOS) Fixed crash on exit when cover manager is open.
* (macOS) Fixed graphical corruption.
* (Windows) Fixed GStreamer registry problems.
* (Windows) Register Tidal URL Scheme in Windows installer.
Enhancements:
* Improved playlist autoscrolling.
* Only allow playlist right click tag editing for editable songs.
* Read song creation time from subsonic API.
* Remember manually set compilation status for albums when songs are rescanned.
* Added icons for edit tag playlist right click menu actions.
* Maximize dialogs if they are already open when clicked again in the menu.
* Added support for compilation tag to edit tag dialog.
* Show song info and album cover in OSD on stop and pause.
* Reshow OSD on song restart.
* Always save initial settings.
* Removed use of deprecated gstreamer "low-percent" (Minimum buffer fill setting).
* Added buffer low and high watermark settings to backend settings.
* Make use of newer version of the desktop notifications service when available.
New features:
* Added setting for enabling scrobbling based on song source.
* Added optional delete from disk in collection and playlist.
* Added Last.fm import data wizard.
* Added smart and dynamic playlists.
* Added song ratings.
* Added Qobuz streaming support.
* Added Subsonic server side scrobbling support.
* Load thumbnails from iPods to show under device collection.
0.7.2:
BugFixes:
Bugfixes:
* Fixed installation directory for translations.
* Fixed collection sorting for non-ASCII characters.
* Fixed closing connected devices on exit.

View File

@@ -1,5 +0,0 @@
from jonaski/opensuse:lp151
run mkdir -p /usr/src/app
workdir /usr/src/app
copy . /usr/src/app

View File

@@ -2,7 +2,7 @@
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/jonaskvinge)
=======================
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt 5 or 6 toolkit.
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt toolkit.
![Browse](https://www.strawberrymusicplayer.org/pictures/screenshot-002-large.png)
@@ -35,7 +35,8 @@ You can also make a one-time payment through [paypal.me/jonaskvinge](https://pay
* Supports WAV, FLAC, WavPack, Ogg FLAC, Ogg Vorbis, Ogg Opus, Ogg Speex, MPC, TrueAudio, AIFF, MP4, MP3, ASF and Monkey's Audio.
* Audio CD playback
* Native desktop notifications
* Playlists in multiple formats
* Playlist management
* Smart and dynamic playlists
* Advanced audio output and device configuration for bit-perfect playback on Linux
* Edit tags on music files
* Fetch tags from MusicBrainz
@@ -46,7 +47,7 @@ You can also make a one-time payment through [paypal.me/jonaskvinge](https://pay
* Audio equalizer
* Transfer music to iPod, MTP or mass-storage USB player
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
* Subsonic and Tidal streaming support
* Subsonic, Tidal and Qobuz streaming support
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
@@ -64,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/)
@@ -83,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:
@@ -96,10 +95,12 @@ With Qt 6 we also depend on the Core5Compat module for QTextCodec.
cd strawberry
mkdir build && cd build
cmake ..
make -j4
make -j$(nproc)
sudo make install
To compile with Qt 6 use: cmake .. -DWITH_QT6=ON
To compile with Qt 6 use:
cmake .. -DBUILD_WITH_QT6=ON
### :penguin: Packaging status

View File

@@ -1,6 +1,8 @@
execute_process(COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macversion.sh OUTPUT_VARIABLE MACOS_VERSION_PACKAGE OUTPUT_STRIP_TRAILING_WHITESPACE)
add_custom_target(dmg
COMMAND /usr/local/opt/qt5/bin/macdeployqt strawberry.app
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macdeploy.py strawberry.app
COMMAND create-dmg --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 strawberry-${STRAWBERRY_VERSION_PACKAGE}.dmg strawberry.app
COMMAND create-dmg --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 strawberry-${STRAWBERRY_VERSION_PACKAGE}-${MACOS_VERSION_PACKAGE}.dmg strawberry.app
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)

View File

@@ -15,7 +15,7 @@ macro(optional_source TOGGLE)
list(APPEND OTHER_SOURCES ${OPTIONAL_SOURCE_HEADERS})
set(_uic_sources)
if(WITH_QT6)
if(BUILD_WITH_QT6)
qt6_wrap_ui(_uic_sources ${OPTIONAL_SOURCE_UI})
else()
qt5_wrap_ui(_uic_sources ${OPTIONAL_SOURCE_UI})

View File

@@ -77,7 +77,7 @@ macro(add_po outfiles po_prefix)
file(APPEND ${_qrc} "<file>${po_prefix}${_lang}.qm</file>")
endforeach(_lang)
file(APPEND ${_qrc} "</qresource></RCC>")
if(WITH_QT6)
if(BUILD_WITH_QT6)
qt6_add_resources(${outfiles} ${_qrc})
else()
qt5_add_resources(${outfiles} ${_qrc})

View File

@@ -1,6 +1,6 @@
set(STRAWBERRY_VERSION_MAJOR 0)
set(STRAWBERRY_VERSION_MINOR 7)
set(STRAWBERRY_VERSION_PATCH 2)
set(STRAWBERRY_VERSION_MINOR 8)
set(STRAWBERRY_VERSION_PATCH 4)
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION OFF)

View File

@@ -13,11 +13,13 @@
<file>schema/schema-10.sql</file>
<file>schema/schema-11.sql</file>
<file>schema/schema-12.sql</file>
<file>schema/schema-13.sql</file>
<file>schema/device-schema.sql</file>
<file>style/strawberry.css</file>
<file>html/playing-tooltip.html</file>
<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>
@@ -40,6 +42,8 @@
<file>pictures/osd_shadow_edge.png</file>
<file>pictures/nyancat.png</file>
<file>pictures/rainbowdash.png</file>
<file>pictures/star-on.png</file>
<file>pictures/star-off.png</file>
<file>fonts/HumongousofEternitySt.ttf</file>
<file>mood/sample.mood</file>
<file>text/ghosts.txt</file>

View File

@@ -1,41 +0,0 @@
<table cellspacing="5" cellpadding="5">
<tr>
<td colspan="%columns">
<center><h4>%appName</h4></center>
</td>
</tr>
<tr>
%image
<td>
<table cellspacing="1" cellpadding="1">
<tr>
<td>
<p align="right">%titleKey</p>
</td>
<td>%titleValue</td>
</tr>
<tr>
<td>
<p align="right">%artistKey</p>
</td>
<td>%artistValue</td>
</tr>
<tr>
<td>
<p align="right">%albumKey</p>
</td>
<td>%albumValue</td>
</tr>
<tr>
<td colspan="2" height="20" />
</tr>
<tr>
<td>
<p align="right">%lengthKey</p>
</td>
<td>%lengthValue</td>
</tr>
</table>
</td>
</tr>
</table>

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>
@@ -89,6 +90,8 @@
<file>icons/128x128/love.png</file>
<file>icons/128x128/subsonic.png</file>
<file>icons/128x128/tidal.png</file>
<file>icons/128x128/qobuz.png</file>
<file>icons/128x128/multimedia-player-ipod-standard-black.png</file>
<file>icons/64x64/albums.png</file>
<file>icons/64x64/alsa.png</file>
<file>icons/64x64/application-exit.png</file>
@@ -162,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>
@@ -179,6 +183,8 @@
<file>icons/64x64/love.png</file>
<file>icons/64x64/subsonic.png</file>
<file>icons/64x64/tidal.png</file>
<file>icons/64x64/qobuz.png</file>
<file>icons/64x64/multimedia-player-ipod-standard-black.png</file>
<file>icons/48x48/albums.png</file>
<file>icons/48x48/alsa.png</file>
<file>icons/48x48/application-exit.png</file>
@@ -256,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>
@@ -273,6 +280,8 @@
<file>icons/48x48/love.png</file>
<file>icons/48x48/subsonic.png</file>
<file>icons/48x48/tidal.png</file>
<file>icons/48x48/qobuz.png</file>
<file>icons/48x48/multimedia-player-ipod-standard-black.png</file>
<file>icons/32x32/albums.png</file>
<file>icons/32x32/alsa.png</file>
<file>icons/32x32/application-exit.png</file>
@@ -350,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>
@@ -367,6 +377,8 @@
<file>icons/32x32/love.png</file>
<file>icons/32x32/subsonic.png</file>
<file>icons/32x32/tidal.png</file>
<file>icons/32x32/qobuz.png</file>
<file>icons/32x32/multimedia-player-ipod-standard-black.png</file>
<file>icons/22x22/albums.png</file>
<file>icons/22x22/alsa.png</file>
<file>icons/22x22/application-exit.png</file>
@@ -444,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>
@@ -461,5 +474,7 @@
<file>icons/22x22/love.png</file>
<file>icons/22x22/subsonic.png</file>
<file>icons/22x22/tidal.png</file>
<file>icons/22x22/qobuz.png</file>
<file>icons/22x22/multimedia-player-ipod-standard-black.png</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

BIN
data/icons/22x22/qobuz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
data/icons/32x32/qobuz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
data/icons/48x48/qobuz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
data/icons/64x64/qobuz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
data/icons/full/qobuz.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

BIN
data/pictures/star-off.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

BIN
data/pictures/star-on.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View File

@@ -62,7 +62,9 @@ CREATE TABLE device_%deviceid_songs (
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT
cue_path TEXT,
rating INTEGER DEFAULT -1
);
@@ -75,4 +77,4 @@ CREATE VIRTUAL TABLE device_%deviceid_fts USING fts5(
tokenize = "unicode61 remove_diacritics 1"
);
UPDATE devices SET schema_version=1 WHERE ROWID=%deviceid;
UPDATE devices SET schema_version=2 WHERE ROWID=%deviceid;

231
data/schema/schema-13.sql Normal file
View File

@@ -0,0 +1,231 @@
ALTER TABLE %allsongstables ADD COLUMN rating INTEGER DEFAULT -1;
ALTER TABLE playlists ADD COLUMN dynamic_playlist_type INTEGER;
ALTER TABLE playlists ADD COLUMN dynamic_playlist_backend TEXT;
ALTER TABLE playlists ADD COLUMN dynamic_playlist_data BLOB;
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
);
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
);
CREATE TABLE IF NOT EXISTS qobuz_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
);
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_artists_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_albums_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
UPDATE schema_version SET version=13;

View File

@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
DELETE FROM schema_version;
INSERT INTO schema_version (version) VALUES (12);
INSERT INTO schema_version (version) VALUES (13);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
@@ -70,178 +70,9 @@ CREATE TABLE IF NOT EXISTS songs (
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT
cue_path TEXT,
);
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT
);
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT
);
CREATE TABLE IF NOT EXISTS tidal_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT
rating INTEGER DEFAULT -1
);
@@ -298,7 +129,363 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT
cue_path TEXT,
rating INTEGER DEFAULT -1
);
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
);
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
);
CREATE TABLE IF NOT EXISTS tidal_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
);
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
);
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
);
CREATE TABLE IF NOT EXISTS qobuz_songs (
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
track INTEGER NOT NULL DEFAULT -1,
disc INTEGER NOT NULL DEFAULT -1,
year INTEGER NOT NULL DEFAULT -1,
originalyear INTEGER NOT NULL DEFAULT -1,
genre TEXT,
compilation INTEGER NOT NULL DEFAULT 0,
composer TEXT,
performer TEXT,
grouping TEXT,
comment TEXT,
lyrics TEXT,
artist_id TEXT,
album_id TEXT,
song_id TEXT,
beginning INTEGER NOT NULL DEFAULT 0,
length INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT -1,
samplerate INTEGER NOT NULL DEFAULT -1,
bitdepth INTEGER NOT NULL DEFAULT -1,
source INTEGER NOT NULL DEFAULT 0,
directory_id INTEGER NOT NULL DEFAULT -1,
url TEXT NOT NULL,
filetype INTEGER NOT NULL DEFAULT 0,
filesize INTEGER NOT NULL DEFAULT -1,
mtime INTEGER NOT NULL DEFAULT -1,
ctime INTEGER NOT NULL DEFAULT -1,
unavailable INTEGER DEFAULT 0,
playcount INTEGER NOT NULL DEFAULT 0,
skipcount INTEGER NOT NULL DEFAULT 0,
lastplayed INTEGER NOT NULL DEFAULT -1,
compilation_detected INTEGER DEFAULT 0,
compilation_on INTEGER NOT NULL DEFAULT 0,
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
);
@@ -309,7 +496,11 @@ CREATE TABLE IF NOT EXISTS playlists (
ui_order INTEGER NOT NULL DEFAULT 0,
special_type TEXT,
ui_path TEXT,
is_favorite INTEGER NOT NULL DEFAULT 0
is_favorite INTEGER NOT NULL DEFAULT 0,
dynamic_playlist_type INTEGER,
dynamic_playlist_backend TEXT,
dynamic_playlist_data BLOB
);
@@ -371,7 +562,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
effective_albumartist TEXT,
effective_originalyear INTEGER,
cue_path TEXT
cue_path TEXT,
rating INTEGER DEFAULT -1
);
@@ -414,6 +607,21 @@ CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts5(
);
CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS tidal_artists_songs_fts USING fts5(
ftstitle,
@@ -459,7 +667,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS tidal_songs_fts USING fts5(
);
CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_artists_songs_fts USING fts5(
ftstitle,
ftsalbum,
@@ -474,7 +682,22 @@ CREATE VIRTUAL TABLE IF NOT EXISTS subsonic_songs_fts USING fts5(
);
CREATE VIRTUAL TABLE IF NOT EXISTS playlist_items_fts_ USING fts5(
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_albums_songs_fts USING fts5(
ftstitle,
ftsalbum,
ftsartist,
ftsalbumartist,
ftscomposer,
ftsperformer,
ftsgrouping,
ftsgenre,
ftscomment,
tokenize = "unicode61 remove_diacritics 1"
);
CREATE VIRTUAL TABLE IF NOT EXISTS qobuz_songs_fts USING fts5(
ftstitle,
ftsalbum,

View File

@@ -0,0 +1,42 @@
#frame {
border: 1px solid palette(mid);
border-radius: 5px;
}
#container {
margin: 2px;
}
#remove {
border-top-left-radius: 0px;
border-top-right-radius: 5px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 5px;
border: 0px solid transparent;
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 %light,
stop:0.4 %light,
stop:0.6 %dark,
stop:1 %dark);
margin-left: 5px;
padding: 0px 5px;
}
#remove:hover {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 %light2,
stop:0.4 %light2,
stop:0.6 %base,
stop:1 %base);
border: 0px solid transparent;
}
#remove:pressed {
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 %base,
stop:0.4 %base,
stop:0.6 %light2,
stop:1 %light2);
border: 0px solid transparent;
}

1
debian/copyright vendored
View File

@@ -12,7 +12,6 @@ Files: src/core/timeconstants.h
ext/libstrawberry-common/core/logging.h
ext/libstrawberry-common/core/messagehandler.cpp
ext/libstrawberry-common/core/messagehandler.h
ext/libstrawberry-common/core/override.h
Copyright: 2011, 2012, David Sansome <me@davidsansome.com>
License: Apache-2.0

View File

@@ -21,7 +21,6 @@ import logging
import os
import re
import subprocess
import commands
import sys
import traceback
@@ -67,56 +66,57 @@ GSTREAMER_SEARCH_PATH = [
GSTREAMER_PLUGINS = [
'libgstapetag.so',
'libgstapp.so',
'libgstaudioconvert.so',
'libgstaudiofx.so',
'libgstaudiomixer.so',
'libgstaudioparsers.so',
'libgstaudiorate.so',
'libgstaudioresample.so',
'libgstaudiotestsrc.so',
'libgstaudiovisualizers.so',
'libgstauparse.so',
'libgstautoconvert.so',
'libgstautodetect.so',
'libgstcoreelements.so',
'libgstequalizer.so',
'libgstgio.so',
'libgsticydemux.so',
'libgstid3demux.so',
'libgstlevel.so',
'libgstosxaudio.so',
'libgstplayback.so',
'libgstrawparse.so',
'libgstrealmedia.so',
'libgstreplaygain.so',
'libgstsoup.so',
'libgstspectrum.so',
'libgsttypefindfunctions.so',
'libgstvolume.so',
'libgstxingmux.so',
'libgsttcp.so',
'libgstudp.so',
'libgstpbtypes.so',
'libgstrtp.so',
'libgstrtsp.so',
'libgstapetag.dylib',
'libgstapp.dylib',
'libgstaudioconvert.dylib',
'libgstaudiofx.dylib',
'libgstaudiomixer.dylib',
'libgstaudioparsers.dylib',
'libgstaudiorate.dylib',
'libgstaudioresample.dylib',
'libgstaudiotestsrc.dylib',
'libgstaudiovisualizers.dylib',
'libgstauparse.dylib',
'libgstautoconvert.dylib',
'libgstautodetect.dylib',
'libgstcoreelements.dylib',
'libgstequalizer.dylib',
'libgstgio.dylib',
'libgsticydemux.dylib',
'libgstid3demux.dylib',
'libgstlevel.dylib',
'libgstosxaudio.dylib',
'libgstplayback.dylib',
'libgstrawparse.dylib',
'libgstrealmedia.dylib',
'libgstreplaygain.dylib',
'libgstsoup.dylib',
'libgstspectrum.dylib',
'libgsttypefindfunctions.dylib',
'libgstvolume.dylib',
'libgstxingmux.dylib',
'libgsttcp.dylib',
'libgstudp.dylib',
'libgstpbtypes.dylib',
'libgstrtp.dylib',
'libgstrtsp.dylib',
'libgstflac.so',
'libgstwavparse.so',
'libgstfaac.so',
'libgstfaad.so',
'libgstogg.so',
'libgstopus.so',
'libgstopusparse.so',
'libgstasf.so',
'libgstspeex.so',
'libgsttaglib.so',
'libgstvorbis.so',
'libgstisomp4.so',
'libgstlibav.so',
'libgstaiff.so',
'libgstlame.so',
'libgstflac.dylib',
'libgstwavparse.dylib',
'libgstfaac.dylib',
'libgstfaad.dylib',
'libgstogg.dylib',
'libgstopus.dylib',
'libgstopusparse.dylib',
'libgstasf.dylib',
'libgstspeex.dylib',
'libgsttaglib.dylib',
'libgstvorbis.dylib',
'libgstisomp4.dylib',
'libgstlibav.dylib',
'libgstaiff.dylib',
'libgstlame.dylib',
'libgstmusepack.dylib',
]
@@ -156,7 +156,7 @@ class CouldNotFindGstreamerPluginError(Error):
pass
if len(sys.argv) < 2:
print 'Usage: %s <bundle.app>' % sys.argv[0]
print('Usage: %s <bundle.app>' % sys.argv[0])
bundle_dir = sys.argv[1]
@@ -176,20 +176,21 @@ fixed_frameworks = set()
def GetBrokenLibraries(binary):
#print "Checking libs for binary: %s" % binary
output = subprocess.Popen([OTOOL, '-L', binary], stdout=subprocess.PIPE).communicate()[0]
#print("Checking libs for binary: %s" % binary)
output = subprocess.Popen([OTOOL, '-L', binary], stdout=subprocess.PIPE).communicate()[0].decode('utf-8')
broken_libs = {'frameworks': [], 'libs': []}
for line in [x.split(' ')[0].lstrip() for x in output.split('\n')[1:]]:
#print "Checking line: %s" % line
#print("Checking line: %s" % line)
if not line: # skip empty lines
continue
if os.path.basename(binary) == os.path.basename(line):
#print "mnope %s-%s" % (os.path.basename(binary), os.path.basename(line))
#print("mnope %s-%s" % (os.path.basename(binary), os.path.basename(line)))
continue
if re.match(r'^\s*/System/', line):
#print("system framework: %s" % line)
continue # System framework
elif re.match(r'^\s*/usr/lib/', line):
#print "unix style system lib"
#print("unix style system lib: %s" % line)
continue # unix style system library
elif re.match(r'^\s*@executable_path', line) or re.match(r'^\s*@rpath', line) or re.match(r'^\s*@loader_path', line):
# Potentially already fixed library
@@ -206,8 +207,9 @@ def GetBrokenLibraries(binary):
if not os.path.exists(os.path.join(frameworks_dir, relative_path)):
broken_libs['frameworks'].append(relative_path)
else:
print "GetBrokenLibraries Error: %s" % line
print("GetBrokenLibraries Error: %s" % line)
elif re.search(r'\w+\.framework', line):
#print("framework: %s" % line)
broken_libs['frameworks'].append(line)
else:
broken_libs['libs'].append(line)
@@ -279,7 +281,7 @@ def FixLibrary(path):
abs_path = FindLibrary(path)
if abs_path == "":
print "Could not resolve %s, not fixing!" % path
print("Could not resolve %s, not fixing!" % path)
return
broken_libs = GetBrokenLibraries(abs_path)
@@ -482,7 +484,7 @@ def main():
try:
FixPlugin('strawberry-tagreader', '.')
except:
print 'Failed to find blob: %s' % traceback.format_exc()
print('Failed to find blob: %s' % traceback.format_exc())
for plugin in GSTREAMER_PLUGINS:
FixPlugin(FindGstreamerPlugin(plugin), 'gstreamer')
@@ -495,11 +497,11 @@ def main():
#FixPlugin(FindQtPlugin(plugin), os.path.dirname(plugin))
if len(sys.argv) <= 2:
print 'Would run %d commands:' % len(commands)
print('Would run %d commands:' % len(commands))
for command in commands:
print ' '.join(command)
print(' '.join(command))
#print 'OK?'
#print('OK?')
#raw_input()
for command in commands:

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

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

View File

@@ -16,85 +16,61 @@
# You should have received a copy of the GNU General Public License
# along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
sizes="128x128 64x64 48x48 32x32 22x22"
sizes="full 128 64 48 32 22"
#
#for i in full/*
#do
# source=$i
# file=`basename $i`
# id=`identify "$i"` || exit 1
# if [ "$id" = "" ] ; then
# echo "ERROR: Cannot determine format and geometry for image: \"$i\"."
# continue
# fi
# g=`echo $id | awk '{print $3}'` || exit 1
# if [ "$g" = "" ] ; then
# echo "ERROR: Cannot determine geometry for image: \"$i\"."
# continue
# fi
# Geometry can be 563x144+0+0 or 75x98
# we need to get rid of the plus (+) and the x characters:
# w=`echo $g | sed 's/[^0-9]/ /g' | awk '{print $1}'` || exit 1
# if [ "$w" = "" ] ; then
# echo "ERROR: Cannot determine width for image: \"$x\"."
# continue
# fi
# h=`echo $g | sed 's/[^0-9]/ /g' | awk '{print $2}'` || exit 1
# if [ "$h" = "" ] ; then
# echo "ERROR: Cannot determine height for image: \"$x\"."
# continue
# fi
# for x in $sizes
# do
# dest="$x/$file"
# if [ -f $dest ]; then
# continue
# fi
# x_w=$(echo $x | cut -d 'x' -f1)
# x_h=$(echo $x | cut -d 'x' -f2)
# if [ "$w" -lt "$x_w" ] || [ "$h" -lt "$x_h" ]; then
# continue
# fi
#echo "convert -verbose -resize $x $source $dest"
#convert -verbose -resize $x $source $dest
# done
#done
for i in $sizes
for s in $sizes
do
for x in $i/*
if [ "$s" = "full" ]; then
dir=$s
else
dir=${s}x${s}
fi
if ! [ -d "$dir" ]; then
echo "Missing $dir directory."
continue
fi
for f in ${dir}/*
do
file=`basename $x`
file=`basename $f`
for y in $sizes
do
if [ "$y" = "$i" ]; then
if [ "$s" = "$y" ]; then
continue
fi
if ! [ -f "$y/$file" ]; then
echo "Warning: $y/$file does not exist, but $x exists."
if [ "$y" = "full" ]; then
dir2=$y
else
dir2=${y}x${y}
fi
if [ "$dir2" = "full" ]; then
continue
fi
if ! [ "$s" = "full" ] && [ $y -gt $s ]; then
continue
fi
if ! [ -f "${dir2}/$file" ]; then
echo "Warning: ${dir2}/$file does not exist, but ${dir}/${file} exists."
fi
done
id=`identify "$x"` || exit 1
if [ "$dir" = "full" ]; then
continue
fi
id=`identify "$f"` || exit 1
if [ "$id" = "" ] ; then
echo "ERROR: Cannot determine format and geometry for image: \"$x\"."
echo "ERROR: Cannot determine format and geometry for image: \"$f\"."
continue
fi
g=`echo $id | awk '{print $3}'` || exit 1
if [ "$g" = "" ] ; then
echo "ERROR: Cannot determine geometry for image: \"$x\"."
echo "ERROR: Cannot determine geometry for image: \"$f\"."
continue
fi
@@ -102,17 +78,17 @@ do
# we need to get rid of the plus (+) and the x characters:
w=`echo $g | sed 's/[^0-9]/ /g' | awk '{print $1}'` || exit 1
if [ "$w" = "" ] ; then
echo "ERROR: Cannot determine width for image: \"$x\"."
echo "ERROR: Cannot determine width for image: \"$f\"."
continue
fi
h=`echo $g | sed 's/[^0-9]/ /g' | awk '{print $2}'` || exit 1
if [ "$h" = "" ] ; then
echo "ERROR: Cannot determine height for image: \"$x\"."
echo "ERROR: Cannot determine height for image: \"$f\"."
continue
fi
if ! [ "${h}x${w}" = "$i" ]; then
echo "Warning: $x is not $i, but ${h}x${w}!"
if ! [ "${h}x${w}" = "$dir" ]; then
echo "Warning: $f is not $dir, but ${h}x${w}!"
fi
done

View File

@@ -80,13 +80,6 @@ BuildRequires: pkgconfig(libvlc)
Requires: libQt5Sql5-sqlite
%endif
%if 0%{?suse_version} && 0%{?suse_version} < 1500
Requires(post): update-desktop-files
Requires(post): gtk3-tools
Requires(postun): update-desktop-files
Requires(postun): gtk3-tools
%endif
%description
Strawberry is a music player and music collection organizer.
It is a fork of Clementine. The name is inspired by the band Strawbs.
@@ -114,62 +107,37 @@ Features:
%setup -qn %{name}-@STRAWBERRY_VERSION_PACKAGE@
%build
%if 0%{?suse_version} || 0%{?mageia}
%{cmake} ..
%else
mkdir -p %{_target_platform}
pushd %{_target_platform}
%{cmake} ..
popd
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
export CXXFLAGS="-fPIC $RPM_OPT_FLAGS"
%endif
%if 0%{?suse_version} || 0%{?mageia}
%if 0%{?suse_version} && 0%{?suse_version} < 1500
make %{?_smp_mflags}
%else
%make_build
%endif
%{cmake} -DCMAKE_BUILD_TYPE:STRING=Release
%if 0%{?centos} || 0%{?mageia}
%make_build
%else
%make_build -C %{_target_platform}
%cmake_build
%endif
%install
%if 0%{?suse_version}
%cmake_install
%if 0%{?centos}
%make_install
%else
%if 0%{?mageia}
%make_install -C build
%else
%make_install -C %{_target_platform}
%endif
%if 0%{?mageia}
%make_install -C build
%else
%cmake_install
%endif
%if 0%{?suse_version} && 0%{?suse_version} < 1500
rm -f %{buildroot}%{_datadir}/metainfo/org.strawberrymusicplayer.strawberry.appdata.xml
%endif
%if 0%{?suse_version}
%suse_update_desktop_file org.strawberrymusicplayer.strawberry Qt AudioVideo Audio Player
%endif
%if 0%{?suse_version} && 0%{?suse_version} < 1500
%post
%desktop_database_post
%icon_theme_cache_post
%postun
%desktop_database_postun
%icon_theme_cache_postun
%endif
%check
desktop-file-validate %{buildroot}%{_datadir}/applications/org.strawberrymusicplayer.strawberry.desktop
%if 0%{?fedora_version}
appstream-util validate-relax --nonet %{buildroot}%{_metainfodir}/org.strawberrymusicplayer.strawberry.appdata.xml
%else
%if ! 0%{?suse_version} || ( 0%{?suse_version} && 0%{?suse_version} >= 1500 )
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/org.strawberrymusicplayer.strawberry.appdata.xml
%endif
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/org.strawberrymusicplayer.strawberry.appdata.xml
%endif
%files
@@ -183,9 +151,7 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/org.strawberrymusicpl
%if 0%{?fedora_version}
%{_metainfodir}/*.appdata.xml
%else
%if ! 0%{?suse_version} || ( 0%{?suse_version} && 0%{?suse_version} >= 1500 )
%{_datadir}/metainfo/*.appdata.xml
%endif
%{_datadir}/metainfo/*.appdata.xml
%endif
%{_mandir}/man1/%{name}.1.*
%{_mandir}/man1/%{name}-tagreader.1.*

View File

@@ -18,7 +18,7 @@
!define debug
!endif
!if "@WITH_QT6@" == "ON"
!if "@BUILD_WITH_QT6@" == "ON"
!define with_qt6
!endif
@@ -174,7 +174,27 @@ Section "Strawberry" Strawberry
File "strawberry.exe"
File "strawberry-tagreader.exe"
File "strawberry.ico"
File "sqlite3.exe"
!ifdef arch_x86
File "libgcc_s_sjlj-1.dll"
File "libcrypto-1_1.dll"
File "libssl-1_1.dll"
!endif
!ifdef arch_x64
File "libgcc_s_seh-1.dll"
File "libcrypto-1_1-x64.dll"
File "libssl-1_1-x64.dll"
!endif
File "avcodec-58.dll"
File "avfilter-7.dll"
File "avformat-58.dll"
File "avresample-4.dll"
File "avutil-56.dll"
File "libbrotlicommon.dll"
File "libbrotlidec.dll"
File "libbz2.dll"
File "libcdio-19.dll"
File "libchromaprint.dll"
@@ -214,21 +234,31 @@ Section "Strawberry" Strawberry
File "libnettle-8.dll"
File "libogg-0.dll"
File "libopus-0.dll"
File "liborc-0.4-0.dll"
File "libpcre-1.dll"
File "libpcre2-16-0.dll"
File "libpng16-16.dll"
File "libprotobuf-23.dll"
File "libprotobuf-25.dll"
File "libpsl-5.dll"
File "libsoup-2.4-1.dll"
File "libspeex-1.dll"
File "libsqlite3-0.dll"
File "libssp-0.dll"
File "libstdc++-6.dll"
File "libtag.dll"
File "libtasn1-6.dll"
File "libunistring-2.dll"
File "libvorbis-0.dll"
File "libvorbisenc-2.dll"
File "libwavpack-1.dll"
File "libwinpthread-1.dll"
File "libxml2-2.dll"
File "libzstd.dll"
File "postproc-55.dll"
File "swresample-3.dll"
File "swscale-5.dll"
File "zlib1.dll"
!ifdef with_qt6
File "Qt6Concurrent.dll"
File "Qt6Core.dll"
@@ -248,24 +278,15 @@ Section "Strawberry" Strawberry
File "Qt5WinExtras.dll"
File "libqtsparkle-qt5.dll"
!endif
File "zlib1.dll"
File "libzstd.dll"
File "libtasn1-6.dll"
File "libbrotlicommon.dll"
File "libbrotlidec.dll"
File "libpsl-5.dll"
File "liborc-0.4-0.dll"
!ifdef arch_x86
File "libgcc_s_sjlj-1.dll"
File "libcrypto-1_1.dll"
File "libssl-1_1.dll"
!endif
!ifdef arch_x64
File "libgcc_s_seh-1.dll"
File "libcrypto-1_1-x64.dll"
File "libssl-1_1-x64.dll"
!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"
@@ -350,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"
@@ -391,6 +412,12 @@ Section "Start menu items" startmenu
SectionEnd
Section "Tidal URL Scheme"
WriteRegStr HKCR "tidal" "URL Protocol" ""
WriteRegStr HKCR "tidal" "" "URL:tidal"
WriteRegStr HKCR 'tidal\shell\open\command' '' '"${PRODUCT_INSTALL_DIR}\strawberry.exe" "%1"'
SectionEnd
Section "Uninstaller"
; Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
@@ -415,7 +442,27 @@ Section "Uninstall"
Delete "$INSTDIR\strawberry.ico"
Delete "$INSTDIR\strawberry.exe"
Delete "$INSTDIR\strawberry-tagreader.exe"
Delete "$INSTDIR\sqlite3.exe"
!ifdef arch_x86
Delete "$INSTDIR\libgcc_s_sjlj-1.dll"
Delete "$INSTDIR\libcrypto-1_1.dll"
Delete "$INSTDIR\libssl-1_1.dll"
!endif
!ifdef arch_x64
Delete "$INSTDIR\libgcc_s_seh-1.dll"
Delete "$INSTDIR\libcrypto-1_1-x64.dll"
Delete "$INSTDIR\libssl-1_1-x64.dll"
!endif
Delete "$INSTDIR\avcodec-58.dll"
Delete "$INSTDIR\avfilter-7.dll"
Delete "$INSTDIR\avformat-58.dll"
Delete "$INSTDIR\avresample-4.dll"
Delete "$INSTDIR\avutil-56.dll"
Delete "$INSTDIR\libbrotlicommon.dll"
Delete "$INSTDIR\libbrotlidec.dll"
Delete "$INSTDIR\libbz2.dll"
Delete "$INSTDIR\libcdio-19.dll"
Delete "$INSTDIR\libchromaprint.dll"
@@ -455,21 +502,29 @@ Section "Uninstall"
Delete "$INSTDIR\libnettle-8.dll"
Delete "$INSTDIR\libogg-0.dll"
Delete "$INSTDIR\libopus-0.dll"
Delete "$INSTDIR\liborc-0.4-0.dll"
Delete "$INSTDIR\libpcre-1.dll"
Delete "$INSTDIR\libpcre2-16-0.dll"
Delete "$INSTDIR\libpng16-16.dll"
Delete "$INSTDIR\libprotobuf-23.dll"
Delete "$INSTDIR\libprotobuf-25.dll"
Delete "$INSTDIR\libpsl-5.dll"
Delete "$INSTDIR\libqtsparkle-qt5.dll"
Delete "$INSTDIR\libqtsparkle-qt6.dll"
Delete "$INSTDIR\libsoup-2.4-1.dll"
Delete "$INSTDIR\libspeex-1.dll"
Delete "$INSTDIR\libsqlite3-0.dll"
Delete "$INSTDIR\libssp-0.dll"
Delete "$INSTDIR\libstdc++-6.dll"
Delete "$INSTDIR\libtag.dll"
Delete "$INSTDIR\libtasn1-6.dll"
Delete "$INSTDIR\libunistring-2.dll"
Delete "$INSTDIR\libvorbis-0.dll"
Delete "$INSTDIR\libvorbisenc-2.dll"
Delete "$INSTDIR\libwavpack-1.dll"
Delete "$INSTDIR\libwinpthread-1.dll"
Delete "$INSTDIR\libxml2-2.dll"
Delete "$INSTDIR\libzstd.dll"
Delete "$INSTDIR\postproc-55.dll"
Delete "$INSTDIR\Qt5Concurrent.dll"
Delete "$INSTDIR\Qt5Core.dll"
Delete "$INSTDIR\Qt5Gui.dll"
@@ -477,7 +532,6 @@ Section "Uninstall"
Delete "$INSTDIR\Qt5Sql.dll"
Delete "$INSTDIR\Qt5Widgets.dll"
Delete "$INSTDIR\Qt5WinExtras.dll"
Delete "$INSTDIR\libqtsparkle-qt5.dll"
Delete "$INSTDIR\Qt6Concurrent.dll"
Delete "$INSTDIR\Qt6Core.dll"
Delete "$INSTDIR\Qt6Gui.dll"
@@ -485,25 +539,18 @@ Section "Uninstall"
Delete "$INSTDIR\Qt6Sql.dll"
Delete "$INSTDIR\Qt6Widgets.dll"
Delete "$INSTDIR\Qt6WinExtras.dll"
Delete "$INSTDIR\libqtsparkle-qt6.dll"
Delete "$INSTDIR\swresample-3.dll"
Delete "$INSTDIR\swscale-5.dll"
Delete "$INSTDIR\zlib1.dll"
Delete "$INSTDIR\libzstd.dll"
Delete "$INSTDIR\libtasn1-6.dll"
Delete "$INSTDIR\libbrotlicommon.dll"
Delete "$INSTDIR\libbrotlidec.dll"
Delete "$INSTDIR\libpsl-5.dll"
Delete "$INSTDIR\liborc-0.4-0.dll"
!ifdef arch_x86
Delete "$INSTDIR\libgcc_s_sjlj-1.dll"
Delete "$INSTDIR\libcrypto-1_1.dll"
Delete "$INSTDIR\libssl-1_1.dll"
!endif
!ifdef arch_x64
Delete "$INSTDIR\libgcc_s_seh-1.dll"
Delete "$INSTDIR\libcrypto-1_1-x64.dll"
Delete "$INSTDIR\libssl-1_1-x64.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"

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

@@ -20,7 +20,7 @@ if(APPLE)
list(APPEND SOURCES core/scoped_nsautorelease_pool.mm)
endif(APPLE)
if(WITH_QT6)
if(BUILD_WITH_QT6)
qt6_wrap_cpp(MOC ${HEADERS})
else()
qt5_wrap_cpp(MOC ${HEADERS})
@@ -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

@@ -21,7 +21,7 @@
#include <functional>
#include <memory>
// Helper for lazy initialisation of objects.
// Helper for lazy initialization of objects.
// Usage:
// Lazy<Foo> my_lazy_object([]() { return new Foo; });
@@ -34,33 +34,32 @@ 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.
void reset() { ptr_.reset(nullptr); }
// 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_());
ptr_.reset(init_(), [](T*obj) { obj->deleteLater(); });
}
}
const std::function<T*()> init_;
mutable std::unique_ptr<T> ptr_;
mutable std::shared_ptr<T> ptr_;
};
#endif // LAZY_H

View File

@@ -1,33 +0,0 @@
/* This file is part of Strawberry.
Copyright 2012, David Sansome <me@davidsansome.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef OVERRIDE_H
#define OVERRIDE_H
// Defines the OVERRIDE macro as C++11's override control keyword if
// it is available.
#ifndef __has_extension
#define __has_extension(x) 0
#endif
#if __has_extension(cxx_override_control) // Clang feature checking macro.
# define OVERRIDE override
#else
# define OVERRIDE
#endif
#endif // OVERRIDE_H

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.0)
set(MESSAGES tagreadermessages.proto)
set(SOURCES fmpsparser.cpp tagreader.cpp)
set(SOURCES tagreader.cpp)
protobuf_generate_cpp(PROTO_SOURCES PROTO_HEADERS ${MESSAGES})
@@ -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(WITH_QT6)
target_link_libraries(libstrawberry-tagreader PRIVATE Qt6::Core5Compat)
endif()

View File

@@ -1,132 +0,0 @@
/* This file is part of Strawberry.
Copyright 2010, David Sansome <me@davidsansome.com>
Strawberry is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Strawberry is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <functional>
#include <QList>
#include <QVariant>
#include <QString>
#include <QChar>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include "fmpsparser.h"
using std::placeholders::_1;
using std::placeholders::_2;
FMPSParser::FMPSParser() :
// The float regex ends with (?:$|(?=::|;;)) to ensure it matches all the way
// up to the end of the value. Without it, it would match a string that
// starts with a number, like "123abc".
float_re_("\\s*([+-]?\\d+(?:\\.\\d+)?)\\s*(?:$|(?=::|;;))"),
// Matches any character except unescaped slashes, colons and semicolons.
string_re_("((?:[^\\\\;:]|(?:\\\\[\\\\:;]))+)(?:$|(?=::|;;))"),
// Used for replacing escaped characters.
escape_re_("\\\\([\\\\:;])") {}
// Parses a list of things (of type T) that are separated by two consecutive
// Separator characters. Each individual thing is parsed by the F function.
// For example, to parse this data:
// foo::bar::baz
// Use:
// QVariantList ret;
// ParseContainer<':'>(data, ParseValue, &ret);
// ret will then contain "foo", "bar", and "baz".
// Returns the number of characters that were consumed from data.
//
// You can parse lists of lists by using different separator characters:
// ParseContainer<';'>(data, ParseContainer<':'>, &ret);
template <char Separator, typename F, typename T>
static int ParseContainer(const QStringRef& data, F f, QList<T>* ret) {
ret->clear();
T value;
int pos = 0;
while (pos < data.length()) {
const int len = data.length() - pos;
int matched_len = f(QStringRef(data.string(), data.position() + pos, len), &value);
if (matched_len == -1 || matched_len > len)
break;
ret->append(value);
pos += matched_len;
// Expect two separators in a row
if (pos + 2 <= data.length() && data.at(pos) == Separator && data.at(pos+1) == Separator) {
pos += 2;
} else {
break;
}
}
return pos;
}
bool FMPSParser::Parse(const QString &data) {
result_ = Result();
// Only return success if we matched the whole string
return ParseListList(data, &result_) == data.length();
}
int FMPSParser::ParseValueRef(const QStringRef& data, QVariant* ret) const {
// Try to match a float
QRegularExpressionMatch re_match = float_re_.match(*data.string(), data.position());
if (re_match.capturedStart() == data.position()) {
*ret = re_match.captured(1).toDouble();
return re_match.capturedLength();
}
// Otherwise try to match a string
re_match = string_re_.match(*data.string(), data.position());
if (re_match.capturedStart() == data.position()) {
// Replace escape sequences with their actual characters
QString value = re_match.captured(1);
value.replace(escape_re_, "\\1");
*ret = value;
return re_match.capturedLength();
}
return -1;
}
// Parses an inner list - a list of values
int FMPSParser::ParseListRef(const QStringRef &data, QVariantList *ret) const {
return ParseContainer<':'>(data, std::bind(&FMPSParser::ParseValueRef, this, _1, _2), ret);
}
// Parses an outer list - a list of lists
int FMPSParser::ParseListListRef(const QStringRef &data, Result *ret) const {
return ParseContainer<';'>(data, std::bind(&FMPSParser::ParseListRef, this, _1, _2), ret);
}
// Convenience functions that take QStrings instead of QStringRefs. Use the QStringRef versions if possible, they're faster.
int FMPSParser::ParseValue(const QString &data, QVariant *ret) const {
return ParseValueRef(QStringRef(&data), ret);
}
int FMPSParser::ParseList(const QString &data, QVariantList *ret) const {
return ParseListRef(QStringRef(&data), ret);
}
int FMPSParser::ParseListList(const QString &data, Result *ret) const {
return ParseListListRef(QStringRef(&data), ret);
}

View File

@@ -1,64 +0,0 @@
/* This file is part of Strawberry.
Copyright 2010, 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 FMPSPARSER_H
#define FMPSPARSER_H
#include "config.h"
#include <QList>
#include <QMetaType>
#include <QString>
#include <QRegularExpression>
class QVariant;
class FMPSParser {
public:
FMPSParser();
// A FMPS result is a list of lists of values (where a value is a string or
// a float).
typedef QList<QVariantList> Result;
// Parses a FMPS value and returns true on success.
bool Parse(const QString &data);
// Gets the result of the last successful Parse.
Result result() const { return result_; }
// Returns true if result() is empty.
bool is_empty() const { return result().isEmpty() || result()[0].isEmpty(); }
// Internal functions, public for unit tests
int ParseValue(const QString &data, QVariant *ret) const;
int ParseValueRef(const QStringRef &data, QVariant *ret) const;
int ParseList(const QString &data, QVariantList *ret) const;
int ParseListRef(const QStringRef &data, QVariantList *ret) const;
int ParseListList(const QString &data, Result *ret) const;
int ParseListListRef(const QStringRef &data, Result *ret) const;
private:
QRegularExpression float_re_;
QRegularExpression string_re_;
QRegularExpression escape_re_;
Result result_;
};
#endif // FMPSPARSER_H

View File

@@ -84,14 +84,11 @@
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QTextCodec>
#include <QVector>
#include <QtDebug>
#include "core/logging.h"
#include "core/messagehandler.h"
#include "fmpsparser.h"
#include "core/timeconstants.h"
class FileRefFactory {
@@ -140,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;
@@ -200,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);
@@ -216,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);
}
@@ -227,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())) {
@@ -257,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();
@@ -282,33 +288,24 @@ 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;
}
}
// Parse FMPS frames
for (uint i = 0; i < map["TXXX"].size(); ++i) {
const TagLib::ID3v2::UserTextIdentificationFrame *frame = dynamic_cast<const TagLib::ID3v2::UserTextIdentificationFrame*>(map["TXXX"][i]);
if (frame && frame->description().startsWith("FMPS_")) {
ParseFMPSFrame(TStringToQString(frame->description()), TStringToQString(frame->fieldList()[1]), song);
}
}
}
}
@@ -323,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());
}
}
@@ -337,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());
}
}
@@ -359,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();
@@ -378,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()) {
@@ -402,7 +399,7 @@ void TagReader::ReadFile(const QString &filename, pb::tagreader::SongMetadata *s
if (compilation.isEmpty()) {
// well, it wasn't set, but if the artist is VA assume it's a compilation
if (QStringFromStdString(song->artist()).toLower() == "various artists") {
if (QStringFromStdString(song->artist()).toLower() == "various artists" || QStringFromStdString(song->albumartist()).toLower() == "various artists") {
song->set_compilation(true);
}
}
@@ -425,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());
@@ -472,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());
}
}
@@ -499,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")) {
@@ -523,40 +503,13 @@ void TagReader::ParseAPETag(const TagLib::APE::ItemListMap &map, const QTextCode
}
void TagReader::ParseFMPSFrame(const QString &name, const QString &value, pb::tagreader::SongMetadata *song) const {
qLog(Debug) << "Parsing FMPSFrame" << name << ", " << value;
FMPSParser parser;
if (!parser.Parse(value) || parser.is_empty()) return;
QVariant var;
if (name == "FMPS_PlayCount") {
var = parser.result()[0][0];
if (var.type() == QVariant::Double) {
song->set_playcount(var.toDouble());
}
}
else if (name == "FMPS_PlayCount_User") {
// Take a user playcount only if there's no playcount already set
if (song->playcount() == 0 && parser.result()[0].count() >= 2) {
var = parser.result()[0][1];
if (var.type() == QVariant::Double) {
song->set_playcount(var.toDouble());
}
}
}
}
void TagReader::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, const pb::tagreader::SongMetadata &song) const {
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
@@ -570,18 +523,22 @@ void TagReader::SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, con
bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetadata &song) const {
if (filename.isNull() || filename.isEmpty()) return false;
if (filename.isEmpty()) return false;
qLog(Debug) << "Saving tags to" << filename;
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();
@@ -609,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())) {
@@ -635,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);
}
@@ -689,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 {
@@ -702,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);
@@ -709,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
@@ -720,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();
@@ -748,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;
@@ -854,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;
@@ -865,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);
@@ -873,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,28 +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 ParseFMPSFrame(const QString &name, const QString &value, 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

@@ -4,7 +4,7 @@ set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
set(SOURCES main.cpp tagreaderworker.cpp)
if(WITH_QT6)
if(BUILD_WITH_QT6)
qt6_wrap_cpp(MOC ${HEADERS})
qt6_add_resources(QRC data/data.qrc)
else()
@@ -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.7.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

@@ -33,12 +33,10 @@ set(SOURCES
core/thread.cpp
core/urlhandler.cpp
core/utilities.cpp
core/scangiomodulepath.cpp
core/iconloader.cpp
core/qtsystemtrayicon.cpp
core/standarditemiconloader.cpp
core/systemtrayicon.cpp
core/screensaver.cpp
core/scopedtransaction.cpp
core/translations.cpp
@@ -96,6 +94,7 @@ set(SOURCES
playlist/playlistview.cpp
playlist/songloaderinserter.cpp
playlist/songplaylistitem.cpp
playlist/dynamicplaylistcontrols.cpp
queue/queue.cpp
queue/queueview.cpp
@@ -111,6 +110,20 @@ set(SOURCES
playlistparsers/xmlparser.cpp
playlistparsers/xspfparser.cpp
smartplaylists/playlistgenerator.cpp
smartplaylists/playlistgeneratorinserter.cpp
smartplaylists/playlistquerygenerator.cpp
smartplaylists/smartplaylistquerywizardplugin.cpp
smartplaylists/smartplaylistsearch.cpp
smartplaylists/smartplaylistsearchpreview.cpp
smartplaylists/smartplaylistsearchterm.cpp
smartplaylists/smartplaylistsearchtermwidget.cpp
smartplaylists/smartplaylistsmodel.cpp
smartplaylists/smartplaylistsviewcontainer.cpp
smartplaylists/smartplaylistsview.cpp
smartplaylists/smartplaylistwizard.cpp
smartplaylists/smartplaylistwizardplugin.cpp
covermanager/albumcovermanager.cpp
covermanager/albumcovermanagerlist.cpp
covermanager/albumcoverloader.cpp
@@ -169,6 +182,9 @@ set(SOURCES
dialogs/trackselectiondialog.cpp
dialogs/addstreamdialog.cpp
dialogs/userpassdialog.cpp
dialogs/deleteconfirmationdialog.cpp
dialogs/lastfmimportdialog.cpp
dialogs/snapdialog.cpp
widgets/autoexpandingtreeview.cpp
widgets/busyindicator.cpp
@@ -192,6 +208,7 @@ set(SOURCES
widgets/tracksliderpopup.cpp
widgets/tracksliderslider.cpp
widgets/loginstatewidget.cpp
widgets/ratingwidget.cpp
osd/osdbase.cpp
osd/osdpretty.cpp
@@ -222,6 +239,7 @@ set(SOURCES
scrobbler/lastfmscrobbler.cpp
scrobbler/librefmscrobbler.cpp
scrobbler/listenbrainzscrobbler.cpp
scrobbler/lastfmimport.cpp
organize/organize.cpp
organize/organizeformat.cpp
@@ -297,6 +315,7 @@ set(HEADERS
playlist/playlistitemmimedata.h
playlist/songloaderinserter.h
playlist/songmimedata.h
playlist/dynamicplaylistcontrols.h
queue/queue.h
queue/queueview.h
@@ -310,6 +329,18 @@ set(HEADERS
playlistparsers/plsparser.h
playlistparsers/xspfparser.h
smartplaylists/playlistgenerator.h
smartplaylists/playlistgeneratorinserter.h
smartplaylists/playlistgeneratormimedata.h
smartplaylists/smartplaylistquerywizardplugin.h
smartplaylists/smartplaylistsearchpreview.h
smartplaylists/smartplaylistsearchtermwidget.h
smartplaylists/smartplaylistsmodel.h
smartplaylists/smartplaylistsviewcontainer.h
smartplaylists/smartplaylistsview.h
smartplaylists/smartplaylistwizard.h
smartplaylists/smartplaylistwizardplugin.h
covermanager/albumcovermanager.h
covermanager/albumcovermanagerlist.h
covermanager/albumcoverloader.h
@@ -367,6 +398,9 @@ set(HEADERS
dialogs/trackselectiondialog.h
dialogs/addstreamdialog.h
dialogs/userpassdialog.h
dialogs/deleteconfirmationdialog.h
dialogs/lastfmimportdialog.h
dialogs/snapdialog.h
widgets/autoexpandingtreeview.h
widgets/busyindicator.h
@@ -390,6 +424,7 @@ set(HEADERS
widgets/tracksliderslider.h
widgets/loginstatewidget.h
widgets/qsearchfield.h
widgets/ratingwidget.h
osd/osdbase.h
osd/osdpretty.h
@@ -418,6 +453,7 @@ set(HEADERS
scrobbler/lastfmscrobbler.h
scrobbler/librefmscrobbler.h
scrobbler/listenbrainzscrobbler.h
scrobbler/lastfmimport.h
organize/organize.h
organize/organizedialog.h
@@ -438,9 +474,17 @@ set(UI
playlist/playlistlistcontainer.ui
playlist/playlistsaveoptionsdialog.ui
playlist/playlistsequence.ui
playlist/dynamicplaylistcontrols.ui
queue/queueview.ui
smartplaylists/smartplaylistquerysearchpage.ui
smartplaylists/smartplaylistquerysortpage.ui
smartplaylists/smartplaylistsearchpreview.ui
smartplaylists/smartplaylistsearchtermwidget.ui
smartplaylists/smartplaylistsviewcontainer.ui
smartplaylists/smartplaylistwizardfinishpage.ui
covermanager/albumcoverexport.ui
covermanager/albumcovermanager.ui
covermanager/albumcoversearcher.ui
@@ -470,6 +514,8 @@ set(UI
dialogs/trackselectiondialog.ui
dialogs/addstreamdialog.ui
dialogs/userpassdialog.ui
dialogs/lastfmimportdialog.ui
dialogs/snapdialog.ui
widgets/trackslider.ui
widgets/fileview.ui
@@ -513,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
@@ -552,7 +598,7 @@ if(UNIX AND HAVE_DBUS)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/dbus)
optional_source(HAVE_DBUS
SOURCES core/mpris.cpp core/mpris2.cpp core/dbusscreensaver.cpp
SOURCES core/mpris.cpp core/mpris2.cpp
HEADERS core/mpris.h core/mpris2.h
)
optional_source(HAVE_UDISKS2
@@ -560,7 +606,7 @@ if(UNIX AND HAVE_DBUS)
HEADERS device/udisks2lister.h
)
if (WITH_QT6)
if (BUILD_WITH_QT6)
# MPRIS 2.0 DBUS interfaces
qt6_add_dbus_adaptor(SOURCES
@@ -588,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
@@ -616,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
@@ -657,7 +719,7 @@ if(UNIX AND HAVE_DBUS)
PROPERTIES NO_NAMESPACE dbus/udisks2drive INCLUDE dbus/metatypes.h)
set_source_files_properties(dbus/org.freedesktop.UDisks2.Job.xml
PROPERTIES NO_NAMESPACE dbus/udisks2job INCLUDE dbus/metatypes.h)
if(WITH_QT6)
if(BUILD_WITH_QT6)
qt6_add_dbus_interface(SOURCES
dbus/org.freedesktop.DBus.ObjectManager.xml
dbus/objectmanager)
@@ -817,7 +879,6 @@ optional_source(APPLE
core/mac_utilities.mm
core/mac_startup.mm
core/macsystemtrayicon.mm
core/macscreensaver.cpp
core/macfslistener.mm
osd/osdmac.mm
widgets/qsearchfield_mac.mm
@@ -856,13 +917,17 @@ optional_source(HAVE_SUBSONIC
subsonic/subsonicurlhandler.cpp
subsonic/subsonicbaserequest.cpp
subsonic/subsonicrequest.cpp
subsonic/subsonicscrobblerequest.cpp
settings/subsonicsettingspage.cpp
scrobbler/subsonicscrobbler.cpp
HEADERS
subsonic/subsonicservice.h
subsonic/subsonicurlhandler.h
subsonic/subsonicbaserequest.h
subsonic/subsonicrequest.h
subsonic/subsonicscrobblerequest.h
settings/subsonicsettingspage.h
scrobbler/subsonicscrobbler.h
UI
settings/subsonicsettingspage.ui
)
@@ -890,6 +955,27 @@ optional_source(HAVE_TIDAL
settings/tidalsettingspage.ui
)
optional_source(HAVE_QOBUZ
SOURCES
qobuz/qobuzservice.cpp
qobuz/qobuzurlhandler.cpp
qobuz/qobuzbaserequest.cpp
qobuz/qobuzrequest.cpp
qobuz/qobuzstreamurlrequest.cpp
qobuz/qobuzfavoriterequest.cpp
settings/qobuzsettingspage.cpp
HEADERS
qobuz/qobuzservice.h
qobuz/qobuzurlhandler.h
qobuz/qobuzbaserequest.h
qobuz/qobuzrequest.h
qobuz/qobuzstreamurlrequest.h
qobuz/qobuzfavoriterequest.h
settings/qobuzsettingspage.h
UI
settings/qobuzsettingspage.ui
)
# Moodbar
optional_source(HAVE_MOODBAR
SOURCES
@@ -915,7 +1001,7 @@ optional_source(HAVE_MOODBAR
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
if(WITH_QT6)
if(BUILD_WITH_QT6)
qt6_wrap_cpp(MOC ${HEADERS})
qt6_wrap_ui(UIC ${UI})
qt6_add_resources(QRC ${RESOURCES})
@@ -963,10 +1049,10 @@ link_directories(
${GNUTLS_LIBRARY_DIRS}
${SQLITE_LIBRARY_DIRS}
${TAGLIB_LIBRARY_DIRS}
${QT_LIBRARY_DIRS}
${SINGLEAPPLICATION_LIBRARY_DIRS}
${SINGLECOREAPPLICATION_LIBRARY_DIRS}
${QTSPARKLE_LIBRARY_DIRS}
${Iconv_LIBRARY_DIRS}
)
if(HAVE_ALSA)
@@ -1013,7 +1099,8 @@ if(HAVE_AUDIOCD)
endif(HAVE_AUDIOCD)
if(HAVE_LIBGPOD)
link_directories(${LIBGPOD_LIBRARIES})
link_directories(${LIBGPOD_LIBRARY_DIRS})
link_directories(${GDK_PIXBUF_LIBRARY_DIRS})
endif(HAVE_LIBGPOD)
if(HAVE_LIBMTP)
@@ -1036,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
@@ -1063,6 +1149,7 @@ target_link_libraries(strawberry_lib PUBLIC
${SINGLEAPPLICATION_LIBRARIES}
${SINGLECOREAPPLICATION_LIBRARIES}
${QTSPARKLE_LIBRARIES}
${Iconv_LIBRARY}
libstrawberry-common
libstrawberry-tagreader
)
@@ -1131,8 +1218,8 @@ if(HAVE_AUDIOCD)
endif(HAVE_AUDIOCD)
if(HAVE_LIBGPOD)
target_include_directories(strawberry_lib SYSTEM PRIVATE ${LIBGPOD_INCLUDE_DIRS})
target_link_libraries(strawberry_lib PRIVATE ${LIBGPOD_LIBRARIES})
target_include_directories(strawberry_lib SYSTEM PRIVATE ${LIBGPOD_INCLUDE_DIRS} ${GDK_PIXBUF_INCLUDE_DIRS})
target_link_libraries(strawberry_lib PRIVATE ${LIBGPOD_LIBRARIES} ${GDK_PIXBUF_LIBRARIES})
endif(HAVE_LIBGPOD)
if(HAVE_LIBMTP)

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

@@ -40,6 +40,7 @@
#include "collectionbackend.h"
#include "collectionmodel.h"
#include "playlist/playlistmanager.h"
#include "scrobbler/lastfmimport.h"
const char *SCollection::kSongsTable = "songs";
const char *SCollection::kDirsTable = "directories";
@@ -106,10 +107,12 @@ void SCollection::Init() {
connect(watcher_, SIGNAL(SubdirsDiscovered(SubdirectoryList)), backend_, SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
connect(watcher_, SIGNAL(SubdirsMTimeUpdated(SubdirectoryList)), backend_, SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
connect(watcher_, SIGNAL(CompilationsNeedUpdating()), backend_, SLOT(UpdateCompilations()));
connect(backend_, SIGNAL(SongsStatisticsChanged(SongList)), SLOT(SongsStatisticsChanged(SongList)));
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(CurrentSongChanged(Song)));
connect(app_->player(), SIGNAL(Stopped()), SLOT(Stopped()));
connect(app_->lastfm_import(), SIGNAL(UpdateLastPlayed(QString, QString, QString, int)), backend_, SLOT(UpdateLastPlayed(QString, QString, QString, int)));
connect(app_->lastfm_import(), SIGNAL(UpdatePlayCount(QString, QString, int)), backend_, SLOT(UpdatePlayCount(QString, QString, int)));
// This will start the watcher checking for updates
backend_->LoadDirectoriesAsync();
@@ -179,7 +182,3 @@ void SCollection::CurrentSongChanged(const Song &song) { // FIXME
}
}
void SCollection::SongsStatisticsChanged(const SongList &songs) {
Q_UNUSED(songs);
}

View File

@@ -79,7 +79,6 @@ class SCollection : public QObject {
void IncrementalScan();
void CurrentSongChanged(const Song &song);
void SongsStatisticsChanged(const SongList& songs);
void Stopped();
signals:

View File

@@ -42,8 +42,10 @@
#include <QSqlDatabase>
#include <QSqlQuery>
#include "core/logging.h"
#include "core/database.h"
#include "core/scopedtransaction.h"
#include "smartplaylists/smartplaylistsearch.h"
#include "directory.h"
#include "collectionbackend.h"
@@ -108,15 +110,15 @@ void CollectionBackend::UpdateTotalAlbumCountAsync() {
metaObject()->invokeMethod(this, "UpdateTotalAlbumCount", Qt::QueuedConnection);
}
void CollectionBackend::IncrementPlayCountAsync(int id) {
void CollectionBackend::IncrementPlayCountAsync(const int id) {
metaObject()->invokeMethod(this, "IncrementPlayCount", Qt::QueuedConnection, Q_ARG(int, id));
}
void CollectionBackend::IncrementSkipCountAsync(int id, float progress) {
void CollectionBackend::IncrementSkipCountAsync(const int id, const float progress) {
metaObject()->invokeMethod(this, "IncrementSkipCount", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, progress));
}
void CollectionBackend::ResetStatisticsAsync(int id) {
void CollectionBackend::ResetStatisticsAsync(const int id) {
metaObject()->invokeMethod(this, "ResetStatistics", Qt::QueuedConnection, Q_ARG(int, id));
}
@@ -133,7 +135,7 @@ void CollectionBackend::LoadDirectories() {
}
void CollectionBackend::ChangeDirPath(int id, const QString &old_path, const QString &new_path) {
void CollectionBackend::ChangeDirPath(const int id, const QString &old_path, const QString &new_path) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
@@ -201,7 +203,7 @@ DirectoryList CollectionBackend::GetAllDirectories() {
}
SubdirectoryList CollectionBackend::SubdirsInDirectory(int id) {
SubdirectoryList CollectionBackend::SubdirsInDirectory(const int id) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db = db_->Connect();
@@ -209,7 +211,7 @@ SubdirectoryList CollectionBackend::SubdirsInDirectory(int id) {
}
SubdirectoryList CollectionBackend::SubdirsInDirectory(int id, QSqlDatabase &db) {
SubdirectoryList CollectionBackend::SubdirsInDirectory(const int id, QSqlDatabase &db) {
QSqlQuery q(db);
q.prepare(QString("SELECT path, mtime FROM %1 WHERE directory_id = :dir").arg(subdirs_table_));
@@ -327,7 +329,7 @@ void CollectionBackend::RemoveDirectory(const Directory &dir) {
}
SongList CollectionBackend::FindSongsInDirectory(int id) {
SongList CollectionBackend::FindSongsInDirectory(const int id) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
@@ -581,7 +583,7 @@ void CollectionBackend::DeleteSongs(const SongList &songs) {
}
void CollectionBackend::MarkSongsUnavailable(const SongList &songs, bool unavailable) {
void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool unavailable) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
@@ -700,7 +702,7 @@ SongList CollectionBackend::ExecCollectionQuery(CollectionQuery *query) {
}
Song CollectionBackend::GetSongById(int id) {
Song CollectionBackend::GetSongById(const int id) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
return GetSongById(id, db);
@@ -749,7 +751,7 @@ SongList CollectionBackend::GetSongsByForeignId(const QStringList &ids, const QS
}
Song CollectionBackend::GetSongById(int id, QSqlDatabase &db) {
Song CollectionBackend::GetSongById(const int id, QSqlDatabase &db) {
SongList list = GetSongsById(QStringList() << QString::number(id), db);
if (list.isEmpty()) return Song();
return list.first();
@@ -851,7 +853,11 @@ Song CollectionBackend::GetSongBySongId(const QString &song_id, QSqlDatabase &db
SongList CollectionBackend::GetSongsBySongId(const QStringList &song_ids, QSqlDatabase &db) {
QString in = song_ids.join(",");
QStringList song_ids2;
for (const QString &song_id : song_ids) {
song_ids2 << "'" + song_id + "'";
}
QString in = song_ids2.join(",");
QSqlQuery q(db);
q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE SONG_ID IN (%2)").arg(songs_table_, in));
@@ -999,10 +1005,8 @@ void CollectionBackend::UpdateCompilations(QSqlQuery &find_song, QSqlQuery &upda
CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist, const bool compilation_required, const QueryOptions &opt) {
AlbumList ret;
CollectionQuery query(opt);
query.SetColumnSpec("album, artist, albumartist, compilation, compilation_detected, art_automatic, art_manual, url");
query.SetColumnSpec("url, artist, albumartist, album, compilation_effective, art_automatic, art_manual");
query.SetOrderBy("album");
if (compilation_required) {
@@ -1015,20 +1019,20 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
{
QMutexLocker l(db_->Mutex());
if (!ExecQuery(&query)) return ret;
if (!ExecQuery(&query)) return AlbumList();
}
QString last_album;
QString last_artist;
QString last_album_artist;
QMap<QString, Album> albums;
while (query.Next()) {
bool is_compilation = query.Value(3).toBool() | query.Value(4).toBool();
bool is_compilation = query.Value(4).toBool();
Album info;
info.artist = is_compilation ? QString() : query.Value(1).toString();
info.album_artist = is_compilation ? QString() : query.Value(2).toString();
info.album_name = query.Value(0).toString();
info.first_url = QUrl::fromEncoded(query.Value(7).toByteArray());
info.first_url = QUrl::fromEncoded(query.Value(0).toByteArray());
if (!is_compilation) {
info.artist = query.Value(1).toString();
info.album_artist = query.Value(2).toString();
}
info.album_name = query.Value(3).toString();
QString art_automatic = query.Value(5).toString();
if (art_automatic.contains(QRegularExpression("..+:.*"))) {
@@ -1046,17 +1050,23 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
info.art_manual = QUrl::fromLocalFile(art_manual);
}
if ((info.artist == last_artist || info.album_artist == last_album_artist) && info.album_name == last_album)
continue;
QString effective_albumartist = info.album_artist.isEmpty() ? info.artist : info.album_artist;
QString key;
if (!effective_albumartist.isEmpty()) {
key.append(effective_albumartist);
}
if (!info.album_name.isEmpty()) {
if (!key.isEmpty()) key.append("-");
key.append(info.album_name);
}
ret << info;
if (key.isEmpty()) continue;
if (!albums.contains(key)) albums.insert(key, info);
last_album = info.album_name;
last_artist = info.artist;
last_album_artist = info.album_artist;
}
return ret;
return albums.values();
}
@@ -1163,7 +1173,7 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &artist, const QStrin
}
void CollectionBackend::ForceCompilation(const QString &album, const QList<QString> &artists, bool on) {
void CollectionBackend::ForceCompilation(const QString &album, const QList<QString> &artists, const bool on) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
@@ -1174,7 +1184,7 @@ void CollectionBackend::ForceCompilation(const QString &album, const QList<QStri
CollectionQuery query;
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("album", album);
if (!artist.isNull() && !artist.isEmpty()) query.AddWhere("artist", artist);
if (!artist.isEmpty()) query.AddWhere("artist", artist);
if (!ExecQuery(&query)) return;
@@ -1219,7 +1229,7 @@ bool CollectionBackend::ExecQuery(CollectionQuery *q) {
return !db_->CheckErrors(q->Exec(db_->Connect(), songs_table_, fts_table_));
}
void CollectionBackend::IncrementPlayCount(int id) {
void CollectionBackend::IncrementPlayCount(const int id) {
if (id == -1) return;
@@ -1238,7 +1248,7 @@ void CollectionBackend::IncrementPlayCount(int id) {
}
void CollectionBackend::IncrementSkipCount(int id, float progress) {
void CollectionBackend::IncrementSkipCount(const int id, const float progress) {
Q_UNUSED(progress);
@@ -1258,7 +1268,7 @@ void CollectionBackend::IncrementSkipCount(int id, float progress) {
}
void CollectionBackend::ResetStatistics(int id) {
void CollectionBackend::ResetStatistics(const int id) {
if (id == -1) return;
@@ -1297,3 +1307,152 @@ void CollectionBackend::DeleteAll() {
emit DatabaseReset();
}
SongList CollectionBackend::FindSongs(const SmartPlaylistSearch &search) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
// Build the query
QString sql = search.ToSql(songs_table());
// Run the query
SongList ret;
QSqlQuery query(db);
query.prepare(sql);
query.exec();
if (db_->CheckErrors(query)) return ret;
// Read the results
while (query.next()) {
Song song;
song.InitFromQuery(query, true);
ret << song;
}
return ret;
}
SongList CollectionBackend::GetAllSongs() {
// Get all the songs!
return FindSongs(SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldAsc, SmartPlaylistSearchTerm::Field_Artist, -1));
}
SongList CollectionBackend::GetSongsBy(const QString &artist, const QString &album, const QString &title) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
SongList songs;
QSqlQuery q(db);
if (album.isEmpty()) {
q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE artist = :artist COLLATE NOCASE AND title = :title COLLATE NOCASE").arg(songs_table_));
}
else {
q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE artist = :artist COLLATE NOCASE AND album = :album COLLATE NOCASE AND title = :title COLLATE NOCASE").arg(songs_table_));
}
q.bindValue(":artist", artist);
if (!album.isEmpty()) q.bindValue(":album", album);
q.bindValue(":title", title);
q.exec();
if (db_->CheckErrors(q)) return SongList();
while (q.next()) {
Song song(source_);
song.InitFromQuery(q, true);
songs << song;
}
return songs;
}
void CollectionBackend::UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const int lastplayed) {
SongList songs = GetSongsBy(artist, album, title);
if (songs.isEmpty()) {
qLog(Debug) << "Could not find a matching song in the database for" << artist << album << title;
return;
}
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
for (const Song &song : songs) {
QSqlQuery q(db);
q.prepare(QString("UPDATE %1 SET lastplayed = :lastplayed WHERE ROWID = :id").arg(songs_table_));
q.bindValue(":lastplayed", lastplayed);
q.bindValue(":id", song.id());
q.exec();
if (db_->CheckErrors(q)) continue;
}
emit SongsStatisticsChanged(SongList() << songs);
}
void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &title, const int playcount) {
SongList songs = GetSongsBy(artist, QString(), title);
if (songs.isEmpty()) {
qLog(Debug) << "Could not find a matching song in the database for" << artist << title;
return;
}
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
for (const Song &song : songs) {
QSqlQuery q(db);
q.prepare(QString("UPDATE %1 SET playcount = :playcount WHERE ROWID = :id").arg(songs_table_));
q.bindValue(":playcount", playcount);
q.bindValue(":id", song.id());
q.exec();
if (db_->CheckErrors(q)) continue;
}
emit SongsStatisticsChanged(SongList() << songs);
}
void CollectionBackend::UpdateSongRating(const int id, const float rating) {
if (id == -1) return;
QList<int> id_list;
id_list << id;
UpdateSongsRating(id_list, rating);
}
void CollectionBackend::UpdateSongsRating(const QList<int> &id_list, const float rating) {
if (id_list.isEmpty()) return;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QStringList id_str_list;
for (int i : id_list) {
id_str_list << QString::number(i);
}
QString ids = id_str_list.join(",");
QSqlQuery q(db);
q.prepare(QString("UPDATE %1 SET rating = :rating WHERE ROWID IN (%2)").arg(songs_table_, ids));
q.bindValue(":rating", rating);
q.exec();
if (db_->CheckErrors(q)) return;
SongList new_song_list = GetSongsById(id_str_list, db);
emit SongsRatingChanged(new_song_list);
}
void CollectionBackend::UpdateSongRatingAsync(const int id, const float rating) {
metaObject()->invokeMethod(this, "UpdateSongRating", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, rating));
}
void CollectionBackend::UpdateSongsRatingAsync(const QList<int>& ids, const float rating) {
metaObject()->invokeMethod(this, "UpdateSongsRating", Qt::QueuedConnection, Q_ARG(QList<int>, ids), Q_ARG(float, rating));
}

View File

@@ -40,6 +40,7 @@
class QThread;
class Database;
class SmartPlaylistSearch;
class CollectionBackendInterface : public QObject {
Q_OBJECT
@@ -80,10 +81,10 @@ class CollectionBackendInterface : public QObject {
virtual void UpdateTotalArtistCountAsync() = 0;
virtual void UpdateTotalAlbumCountAsync() = 0;
virtual SongList FindSongsInDirectory(int id) = 0;
virtual SubdirectoryList SubdirsInDirectory(int id) = 0;
virtual SongList FindSongsInDirectory(const int id) = 0;
virtual SubdirectoryList SubdirsInDirectory(const int id) = 0;
virtual DirectoryList GetAllDirectories() = 0;
virtual void ChangeDirPath(int id, const QString &old_path, const QString &new_path) = 0;
virtual void ChangeDirPath(const int id, const QString &old_path, const QString &new_path) = 0;
virtual QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) = 0;
virtual QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) = 0;
@@ -99,7 +100,7 @@ class CollectionBackendInterface : public QObject {
virtual void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url) = 0;
virtual Album GetAlbumArt(const QString &artist, const QString &albumartist, const QString &album) = 0;
virtual Song GetSongById(int id) = 0;
virtual Song GetSongById(const int id) = 0;
// Returns all sections of a song with the given filename. If there's just one section the resulting list will have it's size equal to 1.
virtual SongList GetSongsByUrl(const QUrl &url) = 0;
@@ -182,10 +183,16 @@ class CollectionBackend : public CollectionBackendInterface {
Song GetSongBySongId(const QString &song_id);
SongList GetSongsBySongId(const QStringList &song_ids);
SongList GetAllSongs();
SongList FindSongs(const SmartPlaylistSearch &search);
Song::Source Source() const;
void AddOrUpdateSongsAsync(const SongList &songs);
void UpdateSongRatingAsync(const int id, const float rating);
void UpdateSongsRatingAsync(const QList<int> &ids, const float rating);
public slots:
void Exit();
void LoadDirectories();
@@ -195,7 +202,7 @@ class CollectionBackend : public CollectionBackendInterface {
void AddOrUpdateSongs(const SongList &songs);
void UpdateMTimesOnly(const SongList &songs);
void DeleteSongs(const SongList &songs);
void MarkSongsUnavailable(const SongList &songs, bool unavailable = true);
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
void AddOrUpdateSubdirs(const SubdirectoryList &subdirs);
void UpdateCompilations();
void UpdateManualAlbumArt(const QString &artist, const QString &albumartist, const QString &album, const QUrl &cover_url);
@@ -205,19 +212,27 @@ class CollectionBackend : public CollectionBackendInterface {
void ResetStatistics(const int id);
void SongPathChanged(const Song &song, const QFileInfo &new_file);
signals:
void DirectoryDiscovered(const Directory &dir, const SubdirectoryList &subdirs);
void DirectoryDeleted(const Directory &dir);
SongList GetSongsBy(const QString &artist, const QString &album, const QString &title);
void UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const int lastplayed);
void UpdatePlayCount(const QString &artist, const QString &title, const int playcount);
void SongsDiscovered(const SongList &songs);
void SongsDeleted(const SongList &songs);
void SongsStatisticsChanged(const SongList& songs);
void UpdateSongRating(const int id, const float rating);
void UpdateSongsRating(const QList<int> &id_list, const float rating);
signals:
void DirectoryDiscovered(Directory, SubdirectoryList);
void DirectoryDeleted(Directory);
void SongsDiscovered(SongList);
void SongsDeleted(SongList);
void SongsStatisticsChanged(SongList);
void DatabaseReset();
void TotalSongCountUpdated(const int total);
void TotalArtistCountUpdated(const int total);
void TotalAlbumCountUpdated(const int total);
void TotalSongCountUpdated(int);
void TotalArtistCountUpdated(int);
void TotalAlbumCountUpdated(int);
void SongsRatingChanged(SongList);
void ExitFinished();

View File

@@ -163,6 +163,17 @@ QString CollectionFilterWidget::group_by() {
}
QString CollectionFilterWidget::group_by_version() {
if (settings_prefix_.isEmpty()) {
return QString("group_by_version");
}
else {
return QString("%1_group_by_version").arg(settings_prefix_);
}
}
QString CollectionFilterWidget::group_by(const int number) { return group_by() + QString::number(number); }
void CollectionFilterWidget::UpdateGroupByActions() {
@@ -213,14 +224,26 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(QObject *parent) {
// read saved groupings
QSettings s;
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
QByteArray bytes = s.value(saved.at(i)).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g;
ds >> g;
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
int version = s.value("version").toInt();
if (version == 1) {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version") continue;
QByteArray bytes = s.value(saved.at(i)).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g;
ds >> g;
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
}
}
else {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version") continue;
s.remove(saved.at(i));
}
}
s.endGroup();
QAction *sep2 = new QAction(parent);
sep2->setSeparator(true);
@@ -301,10 +324,18 @@ void CollectionFilterWidget::SetCollectionModel(CollectionModel *model) {
if (!settings_group_.isEmpty()) {
QSettings s;
s.beginGroup(settings_group_);
model_->SetGroupBy(CollectionModel::Grouping(
CollectionModel::GroupBy(s.value(group_by(1), int(CollectionModel::GroupBy_AlbumArtist)).toInt()),
CollectionModel::GroupBy(s.value(group_by(2), int(CollectionModel::GroupBy_AlbumDisc)).toInt()),
CollectionModel::GroupBy(s.value(group_by(3), int(CollectionModel::GroupBy_None)).toInt())));
int version = 0;
if (s.contains(group_by_version())) version = s.value(group_by_version(), 0).toInt();
if (version == 1) {
model_->SetGroupBy(CollectionModel::Grouping(
CollectionModel::GroupBy(s.value(group_by(1), int(CollectionModel::GroupBy_AlbumArtist)).toInt()),
CollectionModel::GroupBy(s.value(group_by(2), int(CollectionModel::GroupBy_AlbumDisc)).toInt()),
CollectionModel::GroupBy(s.value(group_by(3), int(CollectionModel::GroupBy_None)).toInt())));
}
else {
model_->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_AlbumDisc, CollectionModel::GroupBy_None));
}
s.endGroup();
}
}
@@ -327,9 +358,11 @@ void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping &g)
// Save the settings
QSettings s;
s.beginGroup(settings_group_);
s.setValue(group_by_version(), 1);
s.setValue(group_by(1), int(g[0]));
s.setValue(group_by(2), int(g[1]));
s.setValue(group_by(3), int(g[2]));
s.endGroup();
}
// Now make sure the correct action is checked

View File

@@ -77,6 +77,7 @@ class CollectionFilterWidget : public QWidget {
void SetCollectionModel(CollectionModel *model);
QString group_by();
QString group_by_version();
QString group_by(const int number);
void ReloadSettings();

File diff suppressed because it is too large Load Diff

View File

@@ -85,25 +85,27 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
// These values get saved in QSettings - don't change them
enum GroupBy {
GroupBy_None = 0,
GroupBy_Artist = 1,
GroupBy_Album = 2,
GroupBy_YearAlbum = 3,
GroupBy_Year = 4,
GroupBy_Composer = 5,
GroupBy_Genre = 6,
GroupBy_AlbumArtist = 7,
GroupBy_FileType = 8,
GroupBy_Performer = 9,
GroupBy_Grouping = 10,
GroupBy_Bitrate = 11,
GroupBy_Disc = 12,
GroupBy_OriginalYearAlbum = 13,
GroupBy_OriginalYear = 14,
GroupBy_Samplerate = 15,
GroupBy_Bitdepth = 16,
GroupBy_AlbumArtist = 1,
GroupBy_Artist = 2,
GroupBy_Album = 3,
GroupBy_AlbumDisc = 4,
GroupBy_YearAlbum = 5,
GroupBy_YearAlbumDisc = 6,
GroupBy_OriginalYearAlbum = 7,
GroupBy_OriginalYearAlbumDisc = 8,
GroupBy_Disc = 9,
GroupBy_Year = 10,
GroupBy_OriginalYear = 11,
GroupBy_Genre = 12,
GroupBy_Composer = 13,
GroupBy_Performer = 14,
GroupBy_Grouping = 15,
GroupBy_FileType = 16,
GroupBy_Format = 17,
GroupBy_AlbumDisc = 18,
GroupBy_YearAlbumDisc = 19
GroupBy_Samplerate = 18,
GroupBy_Bitdepth = 19,
GroupBy_Bitrate = 20,
GroupByCount = 21,
};
struct Grouping {
@@ -170,6 +172,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
static QString PrettyYearAlbum(const int year, const QString &album);
static QString PrettyAlbumDisc(const QString &album, const int disc);
static QString PrettyYearAlbumDisc(const int year, const QString &album, const int disc);
static QString PrettyDisc(const int disc);
static QString SortText(QString text);
static QString SortTextForNumber(const int number);
static QString SortTextForArtist(QString artist);
@@ -179,7 +182,18 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
quint64 icon_cache_disk_size() { return sIconCache->cacheSize(); }
static bool IsAlbumGrouping(const GroupBy group_by) { return group_by == GroupBy_Album || group_by == GroupBy_YearAlbum || group_by == GroupBy_OriginalYearAlbum || group_by == GroupBy_AlbumDisc || group_by == GroupBy_YearAlbumDisc; }
static bool IsArtistGroupBy(const GroupBy group_by) {
return group_by == CollectionModel::GroupBy_Artist || group_by == CollectionModel::GroupBy_AlbumArtist;
}
static bool IsAlbumGroupBy(const GroupBy group_by) { return group_by == GroupBy_Album || group_by == GroupBy_YearAlbum || group_by == GroupBy_AlbumDisc || group_by == GroupBy_YearAlbumDisc || group_by == GroupBy_OriginalYearAlbum || group_by == GroupBy_OriginalYearAlbumDisc; }
void set_use_lazy_loading(const bool value) { use_lazy_loading_ = value; }
QMap<QString, CollectionItem*> container_nodes(const int i) { return container_nodes_[i]; }
QList<CollectionItem*> song_nodes() const { return song_nodes_.values(); }
int divider_nodes_count() const { return divider_nodes_.count(); }
void ExpandAll(CollectionItem *item = nullptr) const;
signals:
void TotalSongCountUpdated(const int count);
@@ -248,6 +262,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
QString DividerDisplayText(const GroupBy type, const QString &key) const;
// Helpers
static bool IsCompilationArtistNode(const CollectionItem *node) { return node == node->parent->compilation_artist_node_; }
QString AlbumIconPixmapCacheKey(const QModelIndex &idx) const;
QVariant AlbumIcon(const QModelIndex &idx);
QVariant data(const CollectionItem *item, const int role) const;
@@ -288,6 +303,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
bool use_pretty_covers_;
bool show_dividers_;
bool use_disk_cache_;
bool use_lazy_loading_;
AlbumCoverLoaderOptions cover_loader_options_;

View File

@@ -70,6 +70,6 @@ Song CollectionPlaylistItem::Metadata() const {
void CollectionPlaylistItem::SetArtManual(const QUrl &cover_url) {
song_.set_art_manual(cover_url);
temp_metadata_.set_art_manual(cover_url);
if (HasTemporaryMetadata()) temp_metadata_.set_art_manual(cover_url);
}

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

@@ -48,6 +48,8 @@
#include "core/iconloader.h"
#include "core/mimedata.h"
#include "core/utilities.h"
#include "core/musicstorage.h"
#include "core/deletefiles.h"
#include "collection.h"
#include "collectionbackend.h"
#include "collectiondirectorymodel.h"
@@ -61,7 +63,9 @@
# include "device/devicestatefiltermodel.h"
#endif
#include "dialogs/edittagdialog.h"
#include "dialogs/deleteconfirmationdialog.h"
#include "organize/organizedialog.h"
#include "organize/organizeerrordialog.h"
#include "settings/collectionsettingspage.h"
CollectionView::CollectionView(QWidget *parent)
@@ -73,7 +77,23 @@ CollectionView::CollectionView(QWidget *parent)
total_album_count_(-1),
nomusic_(":/pictures/nomusic.png"),
context_menu_(nullptr),
is_in_keyboard_search_(false)
action_load_(nullptr),
action_add_to_playlist_(nullptr),
action_add_to_playlist_enqueue_(nullptr),
action_add_to_playlist_enqueue_next_(nullptr),
action_open_in_new_playlist_(nullptr),
action_organize_(nullptr),
#ifndef Q_OS_WIN
action_copy_to_device_(nullptr),
#endif
action_edit_track_(nullptr),
action_edit_tracks_(nullptr),
action_rescan_songs_(nullptr),
action_show_in_browser_(nullptr),
action_show_in_various_(nullptr),
action_no_show_in_various_(nullptr),
is_in_keyboard_search_(false),
delete_files_(false)
{
setItemDelegate(new CollectionItemDelegate(this));
@@ -211,6 +231,8 @@ void CollectionView::ReloadSettings() {
app_->collection_model()->set_show_dividers(settings.value("show_dividers", true).toBool());
}
delete_files_ = settings.value("delete_files", false).toBool();
settings.endGroup();
}
@@ -225,7 +247,7 @@ void CollectionView::SetApplication(Application *app) {
void CollectionView::SetFilter(CollectionFilterWidget *filter) { filter_ = filter; }
void CollectionView::TotalSongCountUpdated(int count) {
void CollectionView::TotalSongCountUpdated(const int count) {
int old = total_song_count_;
total_song_count_ = count;
@@ -240,7 +262,7 @@ void CollectionView::TotalSongCountUpdated(int count) {
}
void CollectionView::TotalArtistCountUpdated(int count) {
void CollectionView::TotalArtistCountUpdated(const int count) {
int old = total_artist_count_;
total_artist_count_ = count;
@@ -255,7 +277,7 @@ void CollectionView::TotalArtistCountUpdated(int count) {
}
void CollectionView::TotalAlbumCountUpdated(int count) {
void CollectionView::TotalAlbumCountUpdated(const int count) {
int old = total_album_count_;
total_album_count_ = count;
@@ -316,41 +338,41 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
if (!context_menu_) {
context_menu_ = new QMenu(this);
add_to_playlist_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this, SLOT(AddToPlaylist()));
load_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Replace current playlist"), this, SLOT(Load()));
open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist()));
action_add_to_playlist_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this, SLOT(AddToPlaylist()));
action_load_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Replace current playlist"), this, SLOT(Load()));
action_open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist()));
context_menu_->addSeparator();
add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddToPlaylistEnqueue()));
add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue to play next"), this, SLOT(AddToPlaylistEnqueueNext()));
action_add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddToPlaylistEnqueue()));
action_add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue to play next"), this, SLOT(AddToPlaylistEnqueueNext()));
context_menu_->addSeparator();
organize_ = context_menu_->addAction(IconLoader::Load("edit-copy"), tr("Organize files..."), this, SLOT(Organize()));
action_organize_ = context_menu_->addAction(IconLoader::Load("edit-copy"), tr("Organize files..."), this, SLOT(Organize()));
#ifndef Q_OS_WIN
copy_to_device_ = context_menu_->addAction(IconLoader::Load("device"), tr("Copy to device..."), this, SLOT(CopyToDevice()));
action_copy_to_device_ = context_menu_->addAction(IconLoader::Load("device"), tr("Copy to device..."), this, SLOT(CopyToDevice()));
#endif
//delete_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, SLOT(Delete()));
action_delete_files_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, SLOT(Delete()));
context_menu_->addSeparator();
edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit track information..."), this, SLOT(EditTracks()));
edit_tracks_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit tracks information..."), this, SLOT(EditTracks()));
show_in_browser_ = context_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(ShowInBrowser()));
action_edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit track information..."), this, SLOT(EditTracks()));
action_edit_tracks_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit tracks information..."), this, SLOT(EditTracks()));
action_show_in_browser_ = context_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(ShowInBrowser()));
context_menu_->addSeparator();
rescan_songs_ = context_menu_->addAction(tr("Rescan song(s)"), this, SLOT(RescanSongs()));
action_rescan_songs_ = context_menu_->addAction(tr("Rescan song(s)"), this, SLOT(RescanSongs()));
context_menu_->addSeparator();
show_in_various_ = context_menu_->addAction( tr("Show in various artists"), this, SLOT(ShowInVarious()));
no_show_in_various_ = context_menu_->addAction( tr("Don't show in various artists"), this, SLOT(NoShowInVarious()));
action_show_in_various_ = context_menu_->addAction( tr("Show in various artists"), this, SLOT(ShowInVarious()));
action_no_show_in_various_ = context_menu_->addAction( tr("Don't show in various artists"), this, SLOT(NoShowInVarious()));
context_menu_->addSeparator();
context_menu_->addMenu(filter_->menu());
#ifndef Q_OS_WIN
copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
connect(app_->device_manager()->connected_devices_model(), SIGNAL(IsEmptyChanged(bool)), copy_to_device_, SLOT(setDisabled(bool)));
action_copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
connect(app_->device_manager()->connected_devices_model(), SIGNAL(IsEmptyChanged(bool)), action_copy_to_device_, SLOT(setDisabled(bool)));
#endif
}
@@ -376,34 +398,45 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
const bool regular_elements_only = songs_selected == regular_elements && regular_elements > 0;
// in all modes
load_->setEnabled(songs_selected > 0);
add_to_playlist_->setEnabled(songs_selected > 0);
open_in_new_playlist_->setEnabled(songs_selected > 0);
add_to_playlist_enqueue_->setEnabled(songs_selected > 0);
action_load_->setEnabled(songs_selected > 0);
action_add_to_playlist_->setEnabled(songs_selected > 0);
action_open_in_new_playlist_->setEnabled(songs_selected > 0);
action_add_to_playlist_enqueue_->setEnabled(songs_selected > 0);
// if neither edit_track not edit_tracks are available, we show disabled edit_track element
edit_track_->setVisible(regular_editable == 1);
edit_track_->setEnabled(regular_editable == 1);
edit_tracks_->setVisible(regular_editable > 1);
edit_tracks_->setEnabled(regular_editable > 1);
action_edit_track_->setVisible(regular_editable == 1);
action_edit_track_->setEnabled(regular_editable == 1);
action_edit_tracks_->setVisible(regular_editable > 1);
action_edit_tracks_->setEnabled(regular_editable > 1);
rescan_songs_->setVisible(regular_editable > 0);
rescan_songs_->setEnabled(regular_editable > 0);
action_rescan_songs_->setVisible(regular_editable > 0);
action_rescan_songs_->setEnabled(regular_editable > 0);
organize_->setVisible(regular_elements == regular_editable);
action_organize_->setVisible(regular_elements == regular_editable);
#ifndef Q_OS_WIN
copy_to_device_->setVisible(regular_elements == regular_editable);
action_copy_to_device_->setVisible(regular_elements == regular_editable);
#endif
//delete_->setVisible(regular_elements_only);
show_in_various_->setVisible(regular_elements_only);
no_show_in_various_->setVisible(regular_elements_only);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
action_delete_files_->setVisible(regular_elements == regular_editable && delete_files_);
#else
action_delete_files_->setVisible(false);
#endif
action_show_in_various_->setVisible(regular_elements_only);
action_no_show_in_various_->setVisible(regular_elements_only);
// only when all selected items are editable
organize_->setEnabled(regular_elements == regular_editable);
action_organize_->setEnabled(regular_elements == regular_editable);
#ifndef Q_OS_WIN
copy_to_device_->setEnabled(regular_elements == regular_editable);
action_copy_to_device_->setEnabled(regular_elements == regular_editable);
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
action_delete_files_->setEnabled(regular_elements == regular_editable && delete_files_);
#else
action_delete_files_->setEnabled(false);
#endif
//delete_->setEnabled(regular_elements == regular_editable);
context_menu_->popup(e->globalPos());
@@ -413,7 +446,7 @@ void CollectionView::ShowInVarious() { ShowInVarious(true); }
void CollectionView::NoShowInVarious() { ShowInVarious(false); }
void CollectionView::ShowInVarious(bool on) {
void CollectionView::ShowInVarious(const bool on) {
if (!context_menu_index_.isValid()) return;
@@ -421,7 +454,7 @@ void CollectionView::ShowInVarious(bool on) {
// We put through "Various Artists" changes one album at a time,
// to make sure the old album node gets removed (due to all children removed), before the new one gets added
QMultiMap<QString, QString> albums;
for (const Song& song : GetSelectedSongs()) {
for (const Song &song : GetSelectedSongs()) {
if (albums.find(song.album(), song.artist()) == albums.end())
albums.insert(song.album(), song.artist());
}
@@ -440,7 +473,7 @@ void CollectionView::ShowInVarious(bool on) {
if (other_artists.count() > 0) {
if (QMessageBox::question(this,
tr("There are other songs in this album"),
tr("Would you like to move the other songs in this album to Various Artists as well?"),
tr("Would you like to move the other songs on this album to Various Artists as well?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes) == QMessageBox::Yes) {
for (const QString &s : other_artists) {
@@ -619,3 +652,33 @@ int CollectionView::TotalArtists() {
int CollectionView::TotalAlbums() {
return total_album_count_;
}
void CollectionView::Delete() {
if (!delete_files_) return;
SongList selected_songs = GetSelectedSongs();
QStringList files;
for (const Song &song : selected_songs) {
files << song.url().toString();
}
if (DeleteConfirmationDialog::warning(files) != QDialogButtonBox::Yes) return;
// We can cheat and always take the storage of the first directory, since they'll all be FilesystemMusicStorage in a collection and deleting doesn't check the actual directory.
std::shared_ptr<MusicStorage> storage = app_->collection_model()->directory_model()->index(0, 0).data(MusicStorage::Role_Storage).value<std::shared_ptr<MusicStorage>>();
DeleteFiles *delete_files = new DeleteFiles(app_->task_manager(), storage, true);
connect(delete_files, SIGNAL(Finished(SongList)), SLOT(DeleteFinished(SongList)));
delete_files->Start(selected_songs);
}
void CollectionView::DeleteFilesFinished(const SongList &songs_with_errors) {
if (songs_with_errors.isEmpty()) return;
OrganizeErrorDialog *dialog = new OrganizeErrorDialog(this);
dialog->Show(OrganizeErrorDialog::Type_Delete, songs_with_errors);
// It deletes itself when the user closes it
}

View File

@@ -70,9 +70,9 @@ class CollectionView : public AutoExpandingTreeView {
int TotalAlbums();
public slots:
void TotalSongCountUpdated(int count);
void TotalArtistCountUpdated(int count);
void TotalAlbumCountUpdated(int count);
void TotalSongCountUpdated(const int count);
void TotalArtistCountUpdated(const int count);
void TotalAlbumCountUpdated(const int count);
void ReloadSettings();
void FilterReturnPressed();
@@ -88,7 +88,7 @@ class CollectionView : public AutoExpandingTreeView {
void TotalSongCountUpdated_();
void TotalArtistCountUpdated_();
void TotalAlbumCountUpdated_();
void Error(const QString &message);
void Error(QString);
protected:
// QWidget
@@ -109,10 +109,12 @@ class CollectionView : public AutoExpandingTreeView {
void ShowInBrowser();
void ShowInVarious();
void NoShowInVarious();
void Delete();
void DeleteFilesFinished(const SongList &songs_with_errors);
private:
void RecheckIsEmpty();
void ShowInVarious(bool on);
void ShowInVarious(const bool on);
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
void SaveContainerPath(const QModelIndex &child);
@@ -128,27 +130,28 @@ class CollectionView : public AutoExpandingTreeView {
QMenu *context_menu_;
QModelIndex context_menu_index_;
QAction *load_;
QAction *add_to_playlist_;
QAction *add_to_playlist_enqueue_;
QAction *add_to_playlist_enqueue_next_;
QAction *open_in_new_playlist_;
QAction *organize_;
QAction *action_load_;
QAction *action_add_to_playlist_;
QAction *action_add_to_playlist_enqueue_;
QAction *action_add_to_playlist_enqueue_next_;
QAction *action_open_in_new_playlist_;
QAction *action_organize_;
#ifndef Q_OS_WIN
QAction *copy_to_device_;
QAction *action_copy_to_device_;
#endif
QAction *delete_;
QAction *edit_track_;
QAction *edit_tracks_;
QAction *rescan_songs_;
QAction *show_in_browser_;
QAction *show_in_various_;
QAction *no_show_in_various_;
QAction *action_edit_track_;
QAction *action_edit_tracks_;
QAction *action_rescan_songs_;
QAction *action_show_in_browser_;
QAction *action_show_in_various_;
QAction *action_no_show_in_various_;
QAction *action_delete_files_;
std::unique_ptr<OrganizeDialog> organize_dialog_;
std::unique_ptr<EditTagDialog> edit_tag_dialog_;
bool is_in_keyboard_search_;
bool delete_files_;
// Save focus
Song last_selected_song_;

View File

@@ -65,7 +65,7 @@ static const char *kNoMediaFile = ".nomedia";
static const char *kNoMusicFile = ".nomusic";
}
QStringList CollectionWatcher::sValidImages;
QStringList CollectionWatcher::sValidImages = QStringList() << "jpg" << "png" << "gif" << "jpeg";
CollectionWatcher::CollectionWatcher(Song::Source source, QObject *parent)
: QObject(parent),
@@ -90,10 +90,6 @@ CollectionWatcher::CollectionWatcher(Song::Source source, QObject *parent)
rescan_timer_->setInterval(1000);
rescan_timer_->setSingleShot(true);
if (sValidImages.isEmpty()) {
sValidImages << "jpg" << "png" << "gif" << "jpeg";
}
ReloadSettings();
connect(rescan_timer_, SIGNAL(timeout()), SLOT(RescanPathsNow()));
@@ -573,7 +569,8 @@ SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path
// Also, watch out for incorrect media files.
// Playlist parser for CUEs considers every entry in sheet valid and we don't want invalid media getting into collection!
QString file_nfd = file.normalized(QString::NormalizationForm_D);
for (const Song &cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
for (Song &cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
cue_song.set_source(source_);
if (cue_song.url().toLocalFile().normalized(QString::NormalizationForm_D) == file_nfd) {
if (TagReaderClient::Instance()->IsMediaFileBlocking(file)) {
song_list << cue_song;
@@ -812,7 +809,6 @@ void CollectionWatcher::ReloadSettings() {
scan_on_startup_ = s.value("startup_scan", true).toBool();
monitor_ = s.value("monitor", true).toBool();
mark_songs_unavailable_ = s.value("mark_songs_unavailable", false).toBool();
live_scanning_ = s.value("live_scanning", false).toBool();
QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList();
s.endGroup();

View File

@@ -74,44 +74,45 @@ SavedGroupingManager::~SavedGroupingManager() {
QString SavedGroupingManager::GroupByToString(const CollectionModel::GroupBy &g) {
switch (g) {
case CollectionModel::GroupBy_None: {
case CollectionModel::GroupBy_None:
case CollectionModel::GroupByCount: {
return tr("None");
}
case CollectionModel::GroupBy_Artist: {
return tr("Artist");
}
case CollectionModel::GroupBy_AlbumArtist: {
return tr("Album artist");
}
case CollectionModel::GroupBy_Artist: {
return tr("Artist");
}
case CollectionModel::GroupBy_Album: {
return tr("Album");
}
case CollectionModel::GroupBy_AlbumDisc: {
return tr("Album - Disc");
}
case CollectionModel::GroupBy_Disc: {
return tr("Disc");
}
case CollectionModel::GroupBy_Format: {
return tr("Format");
}
case CollectionModel::GroupBy_Genre: {
return tr("Genre");
}
case CollectionModel::GroupBy_Year: {
return tr("Year");
}
case CollectionModel::GroupBy_YearAlbum: {
return tr("Year - Album");
}
case CollectionModel::GroupBy_YearAlbumDisc: {
return tr("Year - Album - Disc");
}
case CollectionModel::GroupBy_OriginalYearAlbum: {
return tr("Original year - Album");
}
case CollectionModel::GroupBy_OriginalYearAlbumDisc: {
return tr("Original year - Album - Disc");
}
case CollectionModel::GroupBy_Disc: {
return tr("Disc");
}
case CollectionModel::GroupBy_Year: {
return tr("Year");
}
case CollectionModel::GroupBy_OriginalYear: {
return tr("Original year");
}
case CollectionModel::GroupBy_OriginalYearAlbum: {
return tr("Original year - Album");
case CollectionModel::GroupBy_Genre: {
return tr("Genre");
}
case CollectionModel::GroupBy_Composer: {
return tr("Composer");
@@ -125,6 +126,9 @@ QString SavedGroupingManager::GroupByToString(const CollectionModel::GroupBy &g)
case CollectionModel::GroupBy_FileType: {
return tr("File type");
}
case CollectionModel::GroupBy_Format: {
return tr("Format");
}
case CollectionModel::GroupBy_Samplerate: {
return tr("Sample rate");
}
@@ -134,9 +138,10 @@ QString SavedGroupingManager::GroupByToString(const CollectionModel::GroupBy &g)
case CollectionModel::GroupBy_Bitrate: {
return tr("Bitrate");
}
default: { return tr("Unknown"); }
}
return tr("Unknown");
}
void SavedGroupingManager::UpdateModel() {
@@ -144,21 +149,33 @@ void SavedGroupingManager::UpdateModel() {
model_->setRowCount(0); // don't use clear, it deletes headers
QSettings s;
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
QByteArray bytes = s.value(saved.at(i)).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g;
ds >> g;
int version = s.value("version").toInt();
if (version == 1) {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version") continue;
QByteArray bytes = s.value(saved.at(i)).toByteArray();
QDataStream ds(&bytes, QIODevice::ReadOnly);
CollectionModel::Grouping g;
ds >> g;
QList<QStandardItem*> list;
list << new QStandardItem(saved.at(i))
<< new QStandardItem(GroupByToString(g.first))
<< new QStandardItem(GroupByToString(g.second))
<< new QStandardItem(GroupByToString(g.third));
QList<QStandardItem*> list;
list << new QStandardItem(saved.at(i))
<< new QStandardItem(GroupByToString(g.first))
<< new QStandardItem(GroupByToString(g.second))
<< new QStandardItem(GroupByToString(g.third));
model_->appendRow(list);
model_->appendRow(list);
}
}
else {
QStringList saved = s.childKeys();
for (int i = 0; i < saved.size(); ++i) {
if (saved.at(i) == "version") continue;
s.remove(saved.at(i));
}
}
s.endGroup();
}
@@ -173,6 +190,7 @@ void SavedGroupingManager::Remove() {
s.remove(model_->item(index.row(), 0)->text());
}
}
s.endGroup();
}
UpdateModel();
filter_->UpdateGroupByActions();

View File

@@ -46,6 +46,7 @@
#cmakedefine HAVE_SUBSONIC
#cmakedefine HAVE_TIDAL
#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_MOODBAR

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);
@@ -365,8 +366,8 @@ void ContextView::ReloadSettings() {
title_fmt_ = s.value(ContextSettingsPage::kSettingsTitleFmt, "%title% - %artist%").toString();
summary_fmt_ = s.value(ContextSettingsPage::kSettingsSummaryFmt, "%album%").toString();
action_show_album_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ALBUM], true).toBool());
action_show_data_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA], true).toBool());
action_show_output_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ENGINE_AND_DEVICE], true).toBool());
action_show_data_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::TECHNICAL_DATA], false).toBool());
action_show_output_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ENGINE_AND_DEVICE], false).toBool());
action_show_albums_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::ALBUMS_BY_ARTIST], false).toBool());
action_show_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::SONG_LYRICS], true).toBool());
action_search_lyrics_->setChecked(s.value(ContextSettingsPage::kSettingsGroupEnable[ContextSettingsPage::ContextSettingsOrder::SEARCH_LYRICS], true).toBool());

View File

@@ -56,7 +56,6 @@
#include "covermanager/discogscoverprovider.h"
#include "covermanager/musicbrainzcoverprovider.h"
#include "covermanager/deezercoverprovider.h"
#include "covermanager/qobuzcoverprovider.h"
#include "covermanager/musixmatchcoverprovider.h"
#include "covermanager/spotifycoverprovider.h"
@@ -69,6 +68,7 @@
#include "lyrics/chartlyricsprovider.h"
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/lastfmimport.h"
#include "internet/internetservices.h"
@@ -81,6 +81,11 @@
# include "covermanager/tidalcoverprovider.h"
#endif
#ifdef HAVE_QOBUZ
# include "qobuz/qobuzservice.h"
# include "covermanager/qobuzcoverprovider.h"
#endif
#ifdef HAVE_MOODBAR
# include "moodbar/moodbarcontroller.h"
# include "moodbar/moodbarloader.h"
@@ -122,11 +127,13 @@ class ApplicationImpl {
cover_providers->AddProvider(new MusicbrainzCoverProvider(app, app));
cover_providers->AddProvider(new DiscogsCoverProvider(app, app));
cover_providers->AddProvider(new DeezerCoverProvider(app, app));
cover_providers->AddProvider(new QobuzCoverProvider(app, app));
cover_providers->AddProvider(new MusixmatchCoverProvider(app, app));
cover_providers->AddProvider(new SpotifyCoverProvider(app, app));
#ifdef HAVE_TIDAL
cover_providers->AddProvider(new TidalCoverProvider(app, app));
#endif
#ifdef HAVE_QOBUZ
cover_providers->AddProvider(new QobuzCoverProvider(app, app));
#endif
cover_providers->ReloadSettings();
return cover_providers;
@@ -156,10 +163,14 @@ class ApplicationImpl {
#endif
#ifdef HAVE_TIDAL
internet_services->AddService(new TidalService(app, internet_services));
#endif
#ifdef HAVE_QOBUZ
internet_services->AddService(new QobuzService(app, internet_services));
#endif
return internet_services;
}),
scrobbler_([=]() { return new AudioScrobbler(app, app); }),
lastfm_import_([=]() { return new LastFMImport(app); }),
#ifdef HAVE_MOODBAR
moodbar_loader_([=]() { return new MoodbarLoader(app, app); }),
@@ -187,6 +198,7 @@ class ApplicationImpl {
Lazy<LyricsProviders> lyrics_providers_;
Lazy<InternetServices> internet_services_;
Lazy<AudioScrobbler> scrobbler_;
Lazy<LastFMImport> lastfm_import_;
#ifdef HAVE_MOODBAR
Lazy<MoodbarLoader> moodbar_loader_;
Lazy<MoodbarController> moodbar_controller_;
@@ -315,6 +327,7 @@ PlaylistBackend *Application::playlist_backend() const { return p_->playlist_bac
PlaylistManager *Application::playlist_manager() const { return p_->playlist_manager_.get(); }
InternetServices *Application::internet_services() const { return p_->internet_services_.get(); }
AudioScrobbler *Application::scrobbler() const { return p_->scrobbler_.get(); }
LastFMImport *Application::lastfm_import() const { return p_->lastfm_import_.get(); }
#ifdef HAVE_MOODBAR
MoodbarController *Application::moodbar_controller() const { return p_->moodbar_controller_.get(); }
MoodbarLoader *Application::moodbar_loader() const { return p_->moodbar_loader_.get(); }

View File

@@ -56,6 +56,7 @@ class CurrentAlbumCoverLoader;
class CoverProviders;
class LyricsProviders;
class AudioScrobbler;
class LastFMImport;
class InternetServices;
#ifdef HAVE_MOODBAR
class MoodbarController;
@@ -93,6 +94,7 @@ class Application : public QObject {
LyricsProviders *lyrics_providers() const;
AudioScrobbler *scrobbler() const;
LastFMImport *lastfm_import() const;
InternetServices *internet_services() const;

View File

@@ -54,7 +54,7 @@
#include "scopedtransaction.h"
const char *Database::kDatabaseFilename = "strawberry.db";
const int Database::kSchemaVersion = 12;
const int Database::kSchemaVersion = 13;
const char *Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1;
@@ -63,7 +63,9 @@ QMutex Database::sNextConnectionIdMutex;
Database::Database(Application *app, QObject *parent, const QString &database_name) :
QObject(parent),
app_(app),
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
mutex_(QMutex::Recursive),
#endif
injected_database_name_(database_name),
query_hash_(0),
startup_schema_version_(-1),
@@ -130,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())
@@ -200,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;
@@ -516,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

@@ -34,6 +34,9 @@
#include <QSqlQuery>
#include <QString>
#include <QStringList>
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
# include <QRecursiveMutex>
#endif
class QThread;
class Application;
@@ -63,7 +66,12 @@ class Database : public QObject {
QSqlDatabase Connect();
void Close();
bool CheckErrors(const QSqlQuery &query);
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QRecursiveMutex *Mutex() { return &mutex_; }
#else
QMutex *Mutex() { return &mutex_; }
#endif
void RecreateAttachedDb(const QString &database_name);
void ExecSchemaCommands(QSqlDatabase &db, const QString &schema, int schema_version, bool in_transaction = false);
@@ -106,7 +114,11 @@ class Database : public QObject {
QString directory_;
QMutex connect_mutex_;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QRecursiveMutex mutex_;
#else
QMutex mutex_;
#endif
// This ID makes the QSqlDatabase name unique to the object as well as the thread
int connection_id_;

View File

@@ -1,49 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QCoreApplication>
#include <QObject>
#include <QDBusInterface>
#include <QDBusReply>
#include <QString>
#include "dbusscreensaver.h"
DBusScreensaver::DBusScreensaver(const QString &service, const QString &path, const QString &interface)
: service_(service), path_(path), interface_(interface) {}
void DBusScreensaver::Inhibit() {
QDBusInterface gnome_screensaver("org.gnome.ScreenSaver", "/", "org.gnome.ScreenSaver");
QDBusReply<quint32> reply = gnome_screensaver.call("Inhibit", QCoreApplication::applicationName(), QObject::tr("Visualizations"));
if (reply.isValid()) {
cookie_ = reply.value();
}
}
void DBusScreensaver::Uninhibit() {
QDBusInterface gnome_screensaver("org.gnome.ScreenSaver", "/", "org.gnome.ScreenSaver");
gnome_screensaver.call("UnInhibit", cookie_);
}

View File

@@ -34,10 +34,11 @@
const int DeleteFiles::kBatchSize = 50;
DeleteFiles::DeleteFiles(TaskManager *task_manager, std::shared_ptr<MusicStorage> storage)
DeleteFiles::DeleteFiles(TaskManager *task_manager, std::shared_ptr<MusicStorage> storage, const bool use_trash)
: thread_(nullptr),
task_manager_(task_manager),
storage_(storage),
use_trash_(use_trash),
started_(false),
task_id_(0),
progress_(0) {
@@ -112,6 +113,7 @@ void DeleteFiles::ProcessSomeFiles() {
MusicStorage::DeleteJob job;
job.metadata_ = song;
job.use_trash_ = use_trash_;
if (!storage_->DeleteFromStorage(job)) {
songs_with_errors_ << song;

View File

@@ -38,7 +38,7 @@ class DeleteFiles : public QObject {
Q_OBJECT
public:
explicit DeleteFiles(TaskManager *task_manager, std::shared_ptr<MusicStorage> storage);
explicit DeleteFiles(TaskManager *task_manager, std::shared_ptr<MusicStorage> storage, const bool use_trash);
~DeleteFiles() override;
static const int kBatchSize;
@@ -59,6 +59,7 @@ signals:
std::shared_ptr<MusicStorage> storage_;
SongList songs_;
bool use_trash_;
bool started_;

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