Compare commits

...

497 Commits
0.7.1 ... 0.8.5

Author SHA1 Message Date
Jonas Kvinge
d3faf18251 Fix typo 2020-12-19 17:42:24 +01:00
Jonas Kvinge
ddcc296af4 Release 0.8.5 2020-12-19 17:38:24 +01:00
Jonas Kvinge
ee765bc210 Update strawberry.spec 2020-12-17 22:49:57 +01:00
Jonas Kvinge
630c67f7d0 Remove -U__STRICT_ANSI__ from compile flags 2020-12-17 19:47:46 +01:00
Jonas Kvinge
e78bb1b29c Use directsoundsink by default because of buggy wasapi plugin 2020-12-13 21:56:25 +01:00
Strawbs Bot
db312a7380 Update translations 2020-12-13 01:02:57 +01:00
Jonas Kvinge
48b4cbe1b9 Update ccpp.yml 2020-12-12 19:05:21 +01:00
Jonas Kvinge
edb1d4c747 Append Qt5 to Windows setups 2020-12-12 12:51:23 +01:00
Jonas Kvinge
128935b55e Use LPCSTR in RegisterWindowMessageA 2020-12-12 12:51:01 +01:00
Jonas Kvinge
d5a0d67c7b Use ScopedWCharArray 2020-12-12 12:50:33 +01:00
Jonas Kvinge
a86640fdee Exclude Windows from find D-Bus in CMake 2020-12-12 12:49:41 +01:00
Jonas Kvinge
03291b27d1 Mark unused 2020-12-12 12:49:35 +01:00
Strawbs Bot
acf81f116f Update translations 2020-12-12 00:51:58 +01:00
Jonas Kvinge
6564c405c1 Update Changelog 2020-12-12 00:50:46 +01:00
Jonas Kvinge
1bd586268c Update some copyrights 2020-12-12 00:33:27 +01:00
Jonas Kvinge
497952611d Fix HTML escaping for custom OSD notifications
Fixes #618
2020-12-11 23:59:38 +01:00
Jonas Kvinge
9149d1baa3 Add back mistakenly removed QT_MIN_VERSION in CMakeLists.txt 2020-12-11 21:01:51 +01:00
Jonas Kvinge
66c60ac5c7 Add back CentOS 8 to CI 2020-12-09 21:44:47 +01:00
Strawbs Bot
971fad4560 Update translations 2020-12-09 19:33:33 +01:00
Jonas Kvinge
8ed1ce4103 Move ThreadSafeNetworkDiskCache to own file 2020-12-09 18:41:07 +01:00
Jonas Kvinge
3112c34d11 Change to NetworkAccessManager 2020-12-09 18:39:37 +01:00
Jonas Kvinge
7ebf3cecc6 Only include Rpm.cmake and Deb.cmake on Linux 2020-12-09 18:29:30 +01:00
Jonas Kvinge
660fa99f9c Simplify Rpm.cmake 2020-12-09 18:09:49 +01:00
Jonas Kvinge
a997e53d8b Update RPM spec for Qt 6 2020-12-09 01:29:38 +01:00
Jonas Kvinge
1d1dd583e2 Remove CentOS from CI 2020-12-08 15:59:53 +01:00
Jonas Kvinge
d490b29265 Use regex 2020-12-07 22:43:00 +01:00
Jonas Kvinge
e8ca64f16b Replace empty quotes with empty strings for values in CUE parser 2020-12-07 22:34:55 +01:00
Jonas Kvinge
380aa7d884 Only set disc and year in CUE parser when valid 2020-12-07 21:57:15 +01:00
Jonas Kvinge
0a31a94ee8 Change double-click in cover manager to open fullsize cover
Fixes #612
2020-12-07 17:23:08 +01:00
Jonas Kvinge
e1d50ce28f Make URL in scrobbler settings clickable 2020-12-07 17:21:34 +01:00
Jonas Kvinge
38cc39f23d Workaround udisks2 mountpoint issue with Qt 6 2020-12-06 18:46:30 +01:00
Jonas Kvinge
8110035d71 Remove unref 2020-12-06 18:45:37 +01:00
Jonas Kvinge
9af409d6d6 Make sure URL is valid in giolister 2020-12-06 18:45:17 +01:00
Jonas Kvinge
8646829853 Update .travis.yml 2020-12-06 11:16:15 +01:00
Jonas Kvinge
b9e87813b1 Add back Travis-CI 2020-12-06 06:45:08 +01:00
Strawbs Bot
138e032746 Update translations 2020-12-05 01:03:29 +01:00
Jonas Kvinge
d94c6b5aba Set active playlist when playlist doubleclick behaviour is enqueue
Fixes #610
2020-12-04 22:11:35 +01:00
Jonas Kvinge
7c03a39316 Update Changelog 2020-12-04 19:38:00 +01:00
Jonas Kvinge
c482e264f4 Rename variable 2020-12-04 19:13:21 +01:00
Nicolas Toussaint
3a9a1f0a94 add CLI play-playlist option, to play given playlist name. (#608)
* add CLI play-playlist option, to play given playlist name.

* Apply MR change requests
2020-12-04 19:08:41 +01:00
Jonas Kvinge
5b5f728f49 Update README.md 2020-12-03 22:24:57 +01:00
Jonas Kvinge
179aac6b35 Update FUNDING.yml 2020-12-03 22:16:22 +01:00
Strawbs Bot
25de9126ca Update translations 2020-11-30 01:19:32 +01:00
Jonas Kvinge
87be5662ad Update .circleci/config.yml 2020-11-29 16:05:02 +01:00
Jonas Kvinge
053a59e3f4 Update .github/workflows/ccpp.yml 2020-11-29 16:03:44 +01:00
Jonas Kvinge
250857b3d4 Update .circleci/config.yml 2020-11-29 07:54:42 +01:00
Jonas Kvinge
dc288c584c Search qpa/qplatformnativeinterface.h for both Qt 5 and 6 2020-11-29 07:50:08 +01:00
Jonas Kvinge
fc02543f15 Make X11Extras and WinExtras optional
The initial release of Qt 6 does not have these.

These modules are not important.
X11Extras is only used for X11 global shortcuts, which in most cases is
overriden by KDE or Gnome shortcuts anyway.
It is also used in OSD Pretty to detect transparency, but this can be
done using private GUI headers instead.

WinExtras is only used in OSD Pretty to make it transparent.
2020-11-29 07:41:30 +01:00
Jonas Kvinge
0808db706f Remove libgstrealmedia.dylib from macdeploy 2020-11-29 06:43:24 +01:00
Strawbs Bot
1e649fc565 Update translations 2020-11-29 01:02:57 +01:00
Strawbs Bot
c523e959d7 Update translations 2020-11-27 01:02:59 +01:00
Strawbs Bot
1422f988c1 Update translations 2020-11-26 01:02:53 +01:00
Strawbs Bot
e86252b934 Update translations 2020-11-25 01:02:17 +01:00
Jonas Kvinge
e9c59b5c31 Add gst-launch-1.0.exe to windows setup 2020-11-24 15:45:24 +01:00
Strawbs Bot
f7bddee0ce Update translations 2020-11-24 01:05:32 +01:00
Jonas Kvinge
d56b76c9e3 Save both ID3v1 and ID3v2
Fixes #598
2020-11-23 22:25:47 +01:00
Jonas Kvinge
a41353ed03 Update progress even if QDateTime::fromString() fails 2020-11-23 21:54:58 +01:00
Strawbs Bot
7ea12cf2f9 Update translations 2020-11-23 01:05:20 +01:00
Jonas Kvinge
5bc5170112 Remove travis 2020-11-22 20:01:45 +01:00
Jonas Kvinge
946ed0c0b5 Check for valid datetime when importing last played 2020-11-22 19:01:53 +01:00
Jonas Kvinge
fcd4e5aca2 Save and restore geometry in edit tag dialog 2020-11-22 03:37:15 +01:00
Jonas Kvinge
6ab6e6d3a8 Format code 2020-11-22 03:36:46 +01:00
Strawbs Bot
0893d01b4a Update translations 2020-11-22 01:02:49 +01:00
Jonas Kvinge
bf04c45ebf Remove unused live scanning option 2020-11-21 09:40:48 +01:00
Jonas Kvinge
2786a1a97e Remove required version from cmake files 2020-11-21 07:41:43 +01:00
Strawbs Bot
cd9a4f1aa9 Update translations 2020-11-21 01:16:20 +01:00
Jonas Kvinge
91e5cafe76 Remove disabling repeat and shuffle buttons
It's more confusing than helpful
2020-11-20 21:48:10 +01:00
Jonas Kvinge
47754951f0 Format code 2020-11-20 21:47:01 +01:00
Strawbs Bot
fbef8ef32e Update translations 2020-11-20 01:08:25 +01:00
Jonas Kvinge
abd5bf3158 Update .github/workflows/ccpp.yml 2020-11-19 21:25:40 +01:00
Jonas Kvinge
fd024368a3 Update .github/workflows/ccpp.yml 2020-11-19 21:24:30 +01:00
Jonas Kvinge
cf4856a0c9 Fix signal not always emitted from messagreply 2020-11-19 18:29:03 +01:00
Jonas Kvinge
1b791c82fc Use qint64 for message request ID 2020-11-19 18:26:50 +01:00
Jonas Kvinge
3a27a3e868 Format code 2020-11-19 18:22:57 +01:00
Jonas Kvinge
7e6c16b287 Change progress and max progress to qint64 in taskmanager 2020-11-19 18:14:42 +01:00
Jonas Kvinge
f877639ed7 Fix repeat/shuffle disabled when a dynamic playlist is open
Fixes #593
2020-11-19 18:12:48 +01:00
Strawbs Bot
846309d4dd Add Swedish 2020-11-19 01:19:58 +01:00
Strawbs Bot
f2239ddfdf Update translations 2020-11-19 01:02:47 +01:00
Jonas Kvinge
742d455b0e Build on Big Sur 2020-11-18 22:48:16 +01:00
Strawbs Bot
20b08a78e7 Update translations 2020-11-18 01:17:34 +01:00
Jonas Kvinge
042da74955 Change return type of qHash with Qt 6 to size_t 2020-11-17 01:22:38 +01:00
Jonas Kvinge
4dcae4ce21 Use QChar::unicode() 2020-11-17 01:22:00 +01:00
Jonas Kvinge
fa0f07f553 Use QString::number() 2020-11-17 01:21:38 +01:00
Jonas Kvinge
39792d76cf Fix PlaylistGenerator::type() 2020-11-17 01:20:45 +01:00
Strawbs Bot
d181ee9101 Update translations 2020-11-17 01:02:12 +01:00
Jonas Kvinge
335ba1a5b0 Disable tagreader checksum tests
These don't match anymore because tags are now removed.
2020-11-15 12:17:35 +01:00
Jonas Kvinge
4934a360f1 Turn back git revision 2020-11-15 12:16:44 +01:00
Strawbs Bot
0559a4c6f2 Update translations 2020-11-15 01:03:31 +01:00
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
Jonas Kvinge
47b5dea95c Release 0.7.2 2020-08-15 23:01:56 +02:00
Jonas Kvinge
b0df63f1e8 Fix translations dir 2020-08-15 23:01:28 +02:00
Jonas Kvinge
3c4209b676 Add more compilation titles 2020-08-15 17:33:54 +02:00
Jonas Kvinge
d51b9a8e0e Add more compilation titles 2020-08-15 17:29:30 +02:00
Jonas Kvinge
3b56125bd2 Increase maximum time step for seeking to 120
Fixes #509
2020-08-15 15:18:58 +02:00
Jonas Kvinge
6e69e39007 Use static_cast instead for destroyed object 2020-08-15 15:16:06 +02:00
Jonas Kvinge
97208cb329 Add --auto to urpmi command for Mageia CI 2020-08-15 11:55:49 +02:00
Jonas Kvinge
414a4a97fb Use unicode option when replacing non-words
Fixes #513
2020-08-15 11:43:14 +02:00
Jonas Kvinge
2a809f96c4 Update Changelog 2020-08-15 11:31:29 +02:00
Jonas Kvinge
17799b03f3 Fix installation directory for translations
Fixes #512
2020-08-15 11:08:47 +02:00
Jonas Kvinge
efc55fc648 Fix typo in snapcraft.yaml 2020-08-15 02:36:54 +02:00
Jonas Kvinge
22bd41211a Turn back git revision 2020-08-15 02:36:37 +02:00
464 changed files with 43132 additions and 14908 deletions

View File

@@ -155,137 +155,6 @@ commands:
hicolor-icon-theme
install_centos_dependencies:
description: Install CentOS dependencies
steps:
- run:
name: Install epel-release
command: dnf install -y epel-release
- run:
name: Install epel-release-latest-8.noarch.rpm
command: dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
- run:
name: Install config-manager
command: dnf install -y 'dnf-command(config-manager)'
- run:
name: PowerTools
command: dnf config-manager --set-enabled PowerTools
- run:
name: DNF Clean All
command: dnf clean all
- run:
name: Update packages
command: dnf update -y
- run:
name: Install CentOS dependencies
command: >
dnf install -y
glibc
gcc-c++
make
libtool
cmake3
rpmdevtools
redhat-lsb-core
git
man
tar
gettext
boost-devel
fuse-devel
dbus-devel
libnotify-devel
gnutls-devel
sqlite-devel
protobuf-devel
protobuf-compiler
alsa-lib-devel
pulseaudio-libs-devel
qt5-devel
qt5-qtbase-devel
qt5-qtx11extras-devel
qt5-qttools-devel
fftw-devel
libchromaprint-devel
libcdio-devel
libgpod-devel
libmtp-devel
libjpeg-devel
cairo-devel
dbus-x11
xorg-x11-server-Xvfb
xorg-x11-xauth
vim-common
desktop-file-utils
libappstream-glib
appstream-data
hicolor-icon-theme
python3-pip
python3-devel
gstreamer1-devel
gstreamer1-plugins-base-devel
install_mageia_dependencies:
description: Install Mageia dependencies
steps:
- run:
name: Update packages
command: urpmi.update -a
- run:
name: Configure auto update
command: urpmi --auto-update
- run:
name: Install dependencies
command: >
urpmi --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 +163,7 @@ commands:
command: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -342,6 +212,7 @@ commands:
command: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -384,7 +255,7 @@ jobs:
build_source:
docker:
- image: opensuse/leap:15.1
- image: opensuse/leap:15.2
steps:
- install_opensuse_dependencies
- checkout
@@ -392,24 +263,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
@@ -435,18 +288,6 @@ jobs:
- build_rpm
build_fedora_31:
docker:
- image: fedora:31
environment:
RPM_BUILD_NCPUS: "2"
steps:
- install_fedora_dependencies
- checkout
- cmake
- build_source
- build_rpm
build_fedora_32:
docker:
- image: fedora:32
@@ -459,27 +300,13 @@ jobs:
- build_source
- build_rpm
build_centos_8:
build_fedora_33:
docker:
- image: centos:8
- image: fedora:33
environment:
RPM_BUILD_NCPUS: "2"
steps:
- install_centos_dependencies
- checkout
- cmake
- build_source
- build_rpm
build_mageia_7:
docker:
- image: mageia:7
environment:
RPM_BUILD_NCPUS: "2"
steps:
- install_mageia_dependencies
- install_fedora_dependencies
- checkout
- cmake
- build_source
@@ -543,10 +370,6 @@ workflows:
only: /.*/
- build_opensuse_tumbleweed:
filters:
tags:
only: /.*/
- build_opensuse_lp151:
filters:
tags:
@@ -557,23 +380,11 @@ workflows:
only: /.*/
- build_fedora_31:
filters:
tags:
only: /.*/
- build_fedora_32:
filters:
tags:
only: /.*/
- build_mageia_7:
filters:
tags:
only: /.*/
- build_centos_8:
- build_fedora_33:
filters:
tags:
only: /.*/

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

1
.github/FUNDING.yml vendored
View File

@@ -1 +1,2 @@
github: jonaski
patreon: jonaskvinge

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
@@ -223,8 +223,8 @@ jobs:
run: rpmbuild -ba ../dist/unix/strawberry.spec
build_opensuse_tumbleweed:
name: Build openSUSE Tumbleweed
build_opensuse_tumbleweed_qt5:
name: Build openSUSE Tumbleweed Qt 5
runs-on: ubuntu-latest
container:
image: opensuse/tumbleweed
@@ -287,7 +287,7 @@ jobs:
- name: Configure CMake
shell: bash
working-directory: build
run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE
run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DBUILD_WITH_QT5=ON
- name: Create source tarball
working-directory: build
run: ../dist/scripts/maketarball.sh
@@ -301,8 +301,8 @@ jobs:
run: rpmbuild -ba ../dist/unix/strawberry.spec
build_opensuse_qt6:
name: Build openSUSE Qt 6
build_opensuse_tumbleweed_qt6:
name: Build openSUSE Tumbleweed Qt 6
runs-on: ubuntu-latest
container:
image: opensuse/tumbleweed
@@ -352,7 +352,7 @@ jobs:
qt6-x11extras-devel
qt6-base-common-devel
qt6-sql-sqlite
qt6-qt5compat-devel
qt6-linguist-devel
libcdio-devel
libgpod-devel
libmtp-devel
@@ -367,10 +367,18 @@ jobs:
- name: Configure CMake
shell: bash
working-directory: build
run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DWITH_QT6=ON
- name: Build
run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DBUILD_WITH_QT6=ON
- name: Create source tarball
working-directory: build
run: cmake --build . --config $BUILD_TYPE
run: ../dist/scripts/maketarball.sh
- name: Create RPM build sources directories
run: mkdir -p ~/rpmbuild/SOURCES /usr/src/packages/SOURCES
- name: Copy source tarball
working-directory: build
run: cp strawberry-*.tar.xz /usr/src/packages/SOURCES/
- name: Build RPM
working-directory: build
run: rpmbuild -ba ../dist/unix/strawberry.spec
build_fedora_32:
@@ -451,6 +459,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
@@ -461,15 +623,21 @@ jobs:
steps:
- uses: actions/checkout@v1.2.0
- name: Install dnf-plugins-core
run: dnf install -y dnf-plugins-core
- name: Install epel-release
run: dnf install -y epel-release
- name: Install config-manager
run: dnf install -y 'dnf-command(config-manager)'
- name: Enable PowerTools
run: dnf config-manager --set-enabled PowerTools
- name: DNF Clean All
- name: Enable powertools
run: dnf config-manager --set-enabled powertools
- name: Clean all
run: dnf clean all
- name: DNF Update
- name: Update
run: dnf update -y
- name: Install CentOS dependencies
@@ -495,10 +663,9 @@ jobs:
gnutls-devel
sqlite-devel
protobuf-devel
protobuf-compiler
protobuf-c
alsa-lib-devel
pulseaudio-libs-devel
qt5-devel
qt5-qtbase-devel
qt5-qtx11extras-devel
qt5-qttools-devel
@@ -519,7 +686,6 @@ jobs:
hicolor-icon-theme
gstreamer1-devel
gstreamer1-plugins-base-devel
- name: Create Build Environment
shell: bash
run: cmake -E make_directory build
@@ -550,14 +716,14 @@ jobs:
- uses: actions/checkout@v1.2.0
- name: Update packages
run: urpmi.update -a
run: urpmi.update --auto -a
- name: Configure auto update
run: urpmi --auto-update
run: urpmi --auto --auto-update
- name: Install Mageia dependencies
run: >
urpmi --force
urpmi --auto --force
urpmi-debuginfo-install
git
tar
@@ -635,6 +801,7 @@ jobs:
run: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -691,6 +858,7 @@ jobs:
run: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -749,6 +917,7 @@ jobs:
run: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -810,6 +979,7 @@ jobs:
run: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -871,6 +1041,7 @@ jobs:
run: >
apt-get update && apt-get install -y
build-essential
dh-make
ssh
git
make
@@ -919,13 +1090,11 @@ jobs:
run: dpkg-buildpackage -b -d -uc -us -nc -j2
build-macos:
name: Build macOS
runs-on: macos-latest
build-macos-catalina:
name: Build macOS Catalina
runs-on: macos-10.15
steps:
- uses: actions/checkout@v1.2.0
- name: Unlink python
run: brew unlink python@2
- name: Install packages
run: >
brew install
@@ -977,10 +1146,79 @@ 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-catalina
path: build/strawberry-*-catalina.dmg
build-macos-bigsur:
name: Build macOS Big Sur
runs-on: macos-11.0
steps:
- uses: actions/checkout@v1.2.0
- name: Install packages
run: >
brew install
glib
pkgconfig
boost
libffi
protobuf
protobuf-c
qt
gettext
gnutls
fftw
sqlite
chromaprint
gstreamer
gst-plugins-base
gst-plugins-good
gst-plugins-bad
gst-plugins-ugly
gst-libav
libcdio
libmtp
create-dmg
taglib
- name: Delete conflicting taglib system headers
shell: bash
run: rm -rf /usr/local/include/taglib
- name: Create Build Environment
shell: bash
run: cmake -E make_directory build
- name: Configure CMake
shell: bash
env:
PKG_CONFIG_PATH: /usr/local/lib/pkgconfig
Qt5_DIR: /usr/local/opt/qt5/lib/cmake
Qt5LinguistTools_DIR: /usr/local/opt/qt5/lib/cmake/Qt5LinguistTools
GST_SCANNER_PATH: /usr/local/opt/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner
GST_PLUGIN_PATH: /usr/local/lib/gstreamer-1.0
working-directory: build
run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DUSE_BUNDLE=ON
- name: Build
working-directory: build
shell: bash
run: cmake --build . --config $BUILD_TYPE
- name: Install
working-directory: build
shell: bash
run: make install
- name: Create DMG
working-directory: build
shell: bash
run: make dmg
- uses: actions/upload-artifact@v2
with:
name: upload-macos-bigsur
path: build/strawberry-*-bigsur.dmg
build-windows:
@@ -1019,10 +1257,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 +1333,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,gst-launch-1.0.exe} .
- name: Copy dependencies
working-directory: build
@@ -1121,6 +1351,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 +1362,50 @@ jobs:
- name: Build Windows installer
working-directory: build
run: makensis strawberry.nsi
upload-macos-catalina:
name: Upload macOS Catalina DMG
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
needs:
- build-macos-catalina
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 'strawberry-*-catalina.dmg'); do
rsync -e "ssh -p 50220 -o StrictHostKeyChecking=no" -va $i travis@echoes.jkvinge.net:/home/travis/builds/macos/catalina/
done
upload-macos-bigsur:
name: Upload macOS Big Sur DMG
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
needs:
- build-macos-bigsur
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 'strawberry-*-bigsur.dmg'); do
rsync -e "ssh -p 50220 -o StrictHostKeyChecking=no" -va $i travis@echoes.jkvinge.net:/home/travis/builds/macos/bigsur/
done

View File

@@ -1,56 +1,42 @@
sudo: required
language: C++
os:
- osx
services:
- docker
compiler:
- gcc
os: osx
osx_image: xcode11.3
compiler: clang
before_install:
- if ! [ "$DEPLOY_KEY_ENC" == "" ]; then
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 400 brew upgrade || echo "Failed"
- travis_wait 400 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 dmg2
after_success:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then ls -lh strawberry*.dmg; fi
- 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;
elif [[ "$TRAVIS_BRANCH" == "macos" ]]; then
rsync -e "ssh -o StrictHostKeyChecking=no" -va strawberry*.dmg travis@echoes.jkvinge.net:/home/travis/builds/macos;
- ls -lh strawberry*.dmg
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [ -f ~/.ssh/id_rsa ]; then
if [[ "$TRAVIS_BRANCH" == "master" ]] || [[ "$TRAVIS_BRANCH" == "travis" ]]; then
rsync -e "ssh -o StrictHostKeyChecking=no" -va strawberry*.dmg travis@echoes.jkvinge.net:/home/travis/builds/macos/mojave/;
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

@@ -11,11 +11,6 @@ include(cmake/Version.cmake)
include(cmake/Summary.cmake)
include(cmake/OptionalSource.cmake)
include(cmake/ParseArguments.cmake)
include(cmake/Rpm.cmake)
include(cmake/Deb.cmake)
include(cmake/Dmg.cmake)
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(LINUX ON)
@@ -27,13 +22,22 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
set(OPENBSD ON)
endif()
set(CMAKE_CXX_STANDARD 11)
if(LINUX)
include(cmake/Rpm.cmake)
include(cmake/Deb.cmake)
endif()
if(APPLE)
include(cmake/Dmg.cmake)
endif()
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
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>
-U__STRICT_ANSI__
$<$<COMPILE_LANGUAGE:C>:-std=c99>
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
-Wall
-Wextra
-Wpedantic
@@ -91,6 +95,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,89 +131,82 @@ 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)
set(QT_COMPONENTS Core Concurrent Widgets Network Sql)
if(X11_FOUND)
list(APPEND QT_COMPONENTS X11Extras)
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)
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)
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()
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)
unset(OPTIONAL_COMPONENTS)
if(QT_MAJOR_VERSION EQUAL 5)
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()
endif()
if(DBUS_FOUND AND NOT WIN32)
list(APPEND QT_COMPONENTS DBus)
endif()
if(X11_FOUND)
list(APPEND OPTIONAL_COMPONENTS X11Extras)
endif()
if(WIN32)
list(APPEND OPTIONAL_COMPONENTS WinExtras)
endif()
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} REQUIRED COMPONENTS ${QT_COMPONENTS} OPTIONAL_COMPONENTS ${OPTIONAL_COMPONENTS})
set(QtCore_LIBRARIES Qt${QT_MAJOR_VERSION}::Core)
set(QtConcurrent_LIBRARIES Qt${QT_MAJOR_VERSION}::Concurrent)
set(QtGui_LIBRARIES Qt${QT_MAJOR_VERSION}::Gui)
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}::Gui 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)
set(HAVE_X11EXTRAS ON)
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)
set(HAVE_WINEXTRAS ON)
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)
@@ -226,6 +224,15 @@ if(X11_FOUND)
endif()
endif(X11_FOUND)
find_path(QPA_QPLATFORMNATIVEINTERFACE_H qpa/qplatformnativeinterface.h PATHS ${Qt${QT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS})
if(QPA_QPLATFORMNATIVEINTERFACE_H)
set(HAVE_QPA_QPLATFORMNATIVEINTERFACE_H ON)
include_directories(${Qt${QT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS})
message(STATUS "Have qpa/qplatformnativeinterface.h header.")
else()
message(STATUS "Missing qpa/qplatformnativeinterface.h header.")
endif()
# TAGLIB
option(USE_SYSTEM_TAGLIB "Use system taglib" OFF)
if(USE_SYSTEM_TAGLIB)
@@ -261,7 +268,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)
@@ -320,6 +327,10 @@ optional_component(GLOBALSHORTCUTS ON "Global shortcuts"
DEPENDS "D-Bus, X11, Windows or macOS" HAVE_GLOBALSHORTCUTS_SUPPORT
)
optional_component(X11_GLOBALSHORTCUTS ON "X11 global shortcuts"
DEPENDS "X11Extras" Qt${QT_MAJOR_VERSION}X11Extras_FOUND
)
optional_component(AUDIOCD ON "Devices: Audio CD support"
DEPENDS "libcdio" LIBCDIO_FOUND
)
@@ -335,6 +346,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 +358,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 +374,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 +399,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>

129
Changelog
View File

@@ -2,6 +2,132 @@ Strawberry Music Player
=======================
ChangeLog
0.8.5:
Bugfixes:
* Fix return type of SmartPlaylistQueryWizardPlugin::type().
* Fix comparison between QChar and integer to use QChar::unicode().
* Fix return type of qHash with Qt 6 to use size_t instead of uint.
* Fix tag edit dialog save process sometimes stuck.
* Fix repeat and shuffle buttons greyed out when a dynamic playlist is open.
* Fix CUE parser handling of values with empty quotes.
* Fix broken year and disc collection groupings with CUE songs.
* Fix HTML escaping showing up in OSD notifications when using custom text.
Enhancements:
* Add Swedish translation.
* Made Qt X11Extras and WinExtras modules optional.
* Save and restore geometry in edit tag dialog.
* Add command line option to play a playlist based on name.
* Change double-click behaviour in cover manager to open fullsize cover.
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:
* Fixed installation directory for translations.
* Fixed collection sorting for non-ASCII characters.
* Fixed closing connected devices on exit.
0.7.1:
Bugfixes:
@@ -14,6 +140,8 @@ ChangeLog
* Fixed adding playlist songs outside the collection when there are multiple files with the same URL.
* Fixed the rescan songs option to work with local songs outside of the collection.
* Fixed problems with editing song metadata in the playlists.
* Fixed saving and restoring playlist scrollbar position when switching between playlists.
* Fixed minor issue in cue parser with date and genre.
* (macOS) Fixed gst-libav plugin issue resulting in MP3 not working.
Enhancements:
@@ -28,6 +156,7 @@ ChangeLog
* Added fatal CMake error for missing protobuf compiler.
* Added support for parsing radio streams metadata with tilde in title.
* Added CMake option to install translation files.
* Increased maximum time step for seeking to 60.
* (Unix) Added playback actions to desktop file.
* (macOS) Hide behaviour settings that are unavailable on macOS.
* (macOS) Fixed compile warnings.

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

@@ -1,8 +1,9 @@
:strawberry: Strawberry Music Player [![Build Status](https://github.com/strawberrymusicplayer/strawberry/workflows/C/C++%20CI/badge.svg)](https://github.com/strawberrymusicplayer/strawberry/actions)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/jonaskvinge)
[![PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/jonaskvinge)
[![Patreon](https://img.shields.io/badge/patreon-donate-green.svg)](https://patreon.com/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 +36,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 +48,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 +66,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 +85,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 +96,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

@@ -9,8 +9,7 @@ if (LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if (DEB_CODENAME)
configure_file(${CMAKE_SOURCE_DIR}/debian/changelog.in ${CMAKE_SOURCE_DIR}/debian/changelog)
if (DEB_CODENAME AND DEB_DATE)
add_custom_target(deb
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${DPKG_BUILDPACKAGE} -b -d -uc -us

View File

@@ -1,6 +1,15 @@
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}
)
add_custom_target(dmg2
COMMAND /usr/local/opt/qt5/bin/macdeployqt strawberry.app
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macdeploy.py strawberry.app
COMMAND create-dmg --skip-jenkins --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

@@ -1,7 +1,5 @@
# From http://www.cmake.org/Wiki/CMakeMacroParseArguments
cmake_minimum_required(VERSION 2.6)
MACRO(PARSE_ARGUMENTS prefix arg_names option_names)
SET(DEFAULT_ARGS)
FOREACH(arg_name ${arg_names})

View File

@@ -17,6 +17,7 @@ if (LSB_RELEASE_EXEC AND RPMBUILD_EXEC)
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if (DIST_NAME)
message(STATUS "Distro Name: ${DIST_NAME}")
if (DIST_RELEASE)
message(STATUS "Distro Release: ${DIST_RELEASE}")
@@ -24,45 +25,40 @@ if (LSB_RELEASE_EXEC AND RPMBUILD_EXEC)
if (DIST_VERSION)
message(STATUS "Distro Version: ${DIST_VERSION}")
endif()
set(RPMBUILD_DIR ~/rpmbuild CACHE STRING "Rpmbuild directory, for the rpm target")
if (${DIST_NAME} STREQUAL "opensuse")
if (DIST_RELEASE)
if (${DIST_RELEASE} STREQUAL "leap")
if (DIST_VERSION)
set(RPM_DISTRO "lp${DIST_VERSION}" CACHE STRING "Suffix of the rpm file")
else()
set(RPM_DISTRO ${DIST_RELEASE} CACHE STRING "Suffix of the rpm file")
endif()
elseif (${DIST_RELEASE} STREQUAL "tumbleweed")
set(RPM_DISTRO ${DIST_RELEASE} CACHE STRING "Suffix of the rpm file")
else ()
set(RPM_DISTRO ${DIST_NAME} CACHE STRING "Suffix of the rpm file")
if (${DIST_NAME} STREQUAL "opensuse" AND DIST_RELEASE)
if (${DIST_RELEASE} STREQUAL "leap")
if (DIST_VERSION)
set(RPM_DISTRO "lp${DIST_VERSION}")
else()
set(RPM_DISTRO ${DIST_RELEASE})
endif()
else()
set(RPM_DISTRO ${DIST_NAME} CACHE STRING "Suffix of the rpm file")
elseif (${DIST_RELEASE} STREQUAL "tumbleweed")
set(RPM_DISTRO ${DIST_RELEASE})
endif()
elseif (${DIST_NAME} STREQUAL "fedora")
if (DIST_VERSION)
set(RPM_DISTRO "fc${DIST_VERSION}" CACHE STRING "Suffix of the rpm file")
else ()
set(RPM_DISTRO ${DIST_NAME} CACHE STRING "Suffix of the rpm file")
endif()
elseif (${DIST_NAME} STREQUAL "mageia")
if (DIST_RELEASE)
set(RPM_DISTRO "mga${DIST_RELEASE}" CACHE STRING "Suffix of the rpm file")
else ()
set(RPM_DISTRO ${DIST_NAME} CACHE STRING "Suffix of the rpm file")
endif()
else()
set(RPM_DISTRO ${DIST_NAME} CACHE STRING "Suffix of the rpm file")
elseif (${DIST_NAME} STREQUAL "fedora" AND DIST_VERSION)
set(RPM_DISTRO "fc${DIST_VERSION}")
elseif (${DIST_NAME} STREQUAL "centos" AND DIST_VERSION)
set(RPM_DISTRO "el${DIST_VERSION}")
elseif (${DIST_NAME} STREQUAL "mageia" AND DIST_RELEASE)
set(RPM_DISTRO "mga${DIST_RELEASE}")
endif()
if(NOT RPM_DISTRO)
set(RPM_DISTRO ${DIST_NAME})
endif()
message(STATUS "RPM Suffix: ${RPM_DISTRO}")
configure_file(${CMAKE_SOURCE_DIR}/dist/unix/strawberry.spec.in ${CMAKE_SOURCE_DIR}/dist/unix/strawberry.spec @ONLY)
add_custom_target(rpm
COMMAND ${CMAKE_SOURCE_DIR}/dist/scripts/maketarball.sh
COMMAND ${CMAKE_COMMAND} -E copy strawberry-${STRAWBERRY_VERSION_PACKAGE}.tar.xz ${RPMBUILD_DIR}/SOURCES/
COMMAND ${RPMBUILD_EXEC} -bs ${CMAKE_SOURCE_DIR}/dist/unix/strawberry.spec
COMMAND ${RPMBUILD_EXEC} -bb ${CMAKE_SOURCE_DIR}/dist/unix/strawberry.spec
)
endif()
endif()

View File

@@ -1,5 +1,3 @@
cmake_minimum_required(VERSION 3.0)
find_program(GETTEXT_XGETTEXT_EXECUTABLE xgettext)
if(NOT GETTEXT_XGETTEXT_EXECUTABLE)
message(FATAL_ERROR "Could not find xgettext executable")
@@ -77,7 +75,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 1)
set(STRAWBERRY_VERSION_MINOR 8)
set(STRAWBERRY_VERSION_PATCH 5)
#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

6
dist/CMakeLists.txt vendored
View File

@@ -1,4 +1,10 @@
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh.in ${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh @ONLY)
if(RPM_DISTRO AND RPM_DATE)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec.in ${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec @ONLY)
endif(RPM_DISTRO AND RPM_DATE)
if(DEB_CODENAME AND DEB_DATE)
configure_file(${CMAKE_SOURCE_DIR}/debian/changelog.in ${CMAKE_SOURCE_DIR}/debian/changelog)
endif(DEB_CODENAME AND DEB_DATE)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/PKGBUILD.in ${CMAKE_CURRENT_SOURCE_DIR}/unix/PKGBUILD @ONLY)
if (APPLE)

View File

@@ -21,7 +21,6 @@ import logging
import os
import re
import subprocess
import commands
import sys
import traceback
@@ -67,56 +66,56 @@ 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',
'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 +155,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 +175,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 +206,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 +280,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 +483,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 +496,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:

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

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

View File

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

@@ -1,12 +1,15 @@
Name: strawberry
Version: @STRAWBERRY_VERSION_RPM_V@
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
Release: @STRAWBERRY_VERSION_RPM_R@%{?dist}
%else
Release: @STRAWBERRY_VERSION_RPM_R@.@RPM_DISTRO@
%endif
Summary: A music player and music collection organizer
Group: Applications/Multimedia
Group: Productivity/Multimedia/Sound/Players
License: GPL-3.0+
URL: https://www.strawberrymusicplayer.org/
Source0: %{name}-@STRAWBERRY_VERSION_PACKAGE@.tar.xz
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
%if 0%{?suse_version} && 0%{?suse_version} > 1325
BuildRequires: libboost_headers-devel
@@ -26,14 +29,11 @@ BuildRequires: update-desktop-files
%if 0%{?suse_version}
BuildRequires: appstream-glib
%else
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos}
BuildRequires: libappstream-glib
%else
%else
BuildRequires: appstream-util
%endif
%endif
%if 0%{?suse_version} || 0%{?fedora_version} || 0%{?mageia}
BuildRequires: cmake(Qt5LinguistTools)
%endif
%endif
BuildRequires: pkgconfig
BuildRequires: pkgconfig(glib-2.0)
@@ -49,15 +49,29 @@ BuildRequires: pkgconfig(sqlite3) >= 3.9
BuildRequires: pkgconfig(taglib)
%endif
BuildRequires: pkgconfig(fftw3)
BuildRequires: pkgconfig(Qt5Core)
BuildRequires: pkgconfig(Qt5Gui)
BuildRequires: pkgconfig(Qt5Widgets)
BuildRequires: pkgconfig(Qt5Concurrent)
BuildRequires: pkgconfig(Qt5Network)
BuildRequires: pkgconfig(Qt5Sql)
BuildRequires: pkgconfig(Qt5X11Extras)
BuildRequires: pkgconfig(Qt5DBus)
BuildRequires: pkgconfig(Qt5Test)
%if "@QT_MAJOR_VERSION@" == "5" && ( 0%{?fedora} || 0%{?rhel_version} || 0%{?centos} )
BuildRequires: pkgconfig(Qt@QT_MAJOR_VERSION@Core)
BuildRequires: pkgconfig(Qt@QT_MAJOR_VERSION@Gui)
BuildRequires: pkgconfig(Qt@QT_MAJOR_VERSION@Widgets)
BuildRequires: pkgconfig(Qt@QT_MAJOR_VERSION@Concurrent)
BuildRequires: pkgconfig(Qt@QT_MAJOR_VERSION@Network)
BuildRequires: pkgconfig(Qt@QT_MAJOR_VERSION@Sql)
BuildRequires: pkgconfig(Qt@QT_MAJOR_VERSION@X11Extras)
BuildRequires: pkgconfig(Qt@QT_MAJOR_VERSION@DBus)
BuildRequires: pkgconfig(Qt@QT_MAJOR_VERSION@Test)
%else
BuildRequires: cmake(Qt@QT_MAJOR_VERSION@Core)
BuildRequires: cmake(Qt@QT_MAJOR_VERSION@Gui)
BuildRequires: cmake(Qt@QT_MAJOR_VERSION@Widgets)
BuildRequires: cmake(Qt@QT_MAJOR_VERSION@Concurrent)
BuildRequires: cmake(Qt@QT_MAJOR_VERSION@Network)
BuildRequires: cmake(Qt@QT_MAJOR_VERSION@Sql)
BuildRequires: cmake(Qt@QT_MAJOR_VERSION@DBus)
BuildRequires: cmake(Qt@QT_MAJOR_VERSION@Test)
%endif
%if 0%{?suse_version} || 0%{?fedora_version} || 0%{?mageia}
BuildRequires: cmake(Qt@QT_MAJOR_VERSION@LinguistTools)
%endif
BuildRequires: pkgconfig(gstreamer-1.0)
BuildRequires: pkgconfig(gstreamer-app-1.0)
BuildRequires: pkgconfig(gstreamer-audio-1.0)
@@ -70,21 +84,17 @@ BuildRequires: pkgconfig(libpulse)
BuildRequires: pkgconfig(libcdio)
BuildRequires: pkgconfig(libgpod-1.0)
BuildRequires: pkgconfig(libmtp)
BuildRequires: pkgconfig(libnotify)
BuildRequires: pkgconfig(libudf)
%if 0%{?suse_version} || 0%{?fedora_version}
BuildRequires: pkgconfig(libvlc)
%endif
%if 0%{?suse_version}
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
%if "@QT_MAJOR_VERSION@" == "6"
Requires: qt6-sql-sqlite
%endif
%if "@QT_MAJOR_VERSION@" == "5"
Requires: libQt5Sql5-sqlite
%endif
%endif
%description
@@ -114,62 +124,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 -DQT_MAJOR_VERSION=@QT_MAJOR_VERSION@
%if 0%{?centos} || (0%{?mageia} && 0%{?mageia} <= 7)
%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 +168,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
@@ -98,13 +98,13 @@ Name "${PRODUCT_NAME}"
!ifdef with_qt6
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt6-Debug-x86.exe"
!else
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Debug-x86.exe"
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt5-Debug-x86.exe"
!endif
!else
!ifdef with_qt6
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt6-x86.exe"
!else
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-x86.exe"
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt5-x86.exe"
!endif
!endif
!endif
@@ -114,13 +114,13 @@ Name "${PRODUCT_NAME}"
!ifdef with_qt6
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt6-Debug-x64.exe"
!else
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Debug-x64.exe"
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt5-Debug-x64.exe"
!endif
!else
!ifdef with_qt6
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt6-x64.exe"
!else
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-x64.exe"
OutFile "${PRODUCT_NAME_SHORT}Setup-${PRODUCT_DISPLAY_VERSION}-Qt5-x64.exe"
!endif
!endif
!endif
@@ -174,7 +174,28 @@ Section "Strawberry" Strawberry
File "strawberry.exe"
File "strawberry-tagreader.exe"
File "strawberry.ico"
File "sqlite3.exe"
File "gst-launch-1.0.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 +235,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"
@@ -236,7 +267,7 @@ Section "Strawberry" Strawberry
File "Qt6Network.dll"
File "Qt6Sql.dll"
File "Qt6Widgets.dll"
File "Qt6WinExtras.dll"
;File "Qt6WinExtras.dll"
File "libqtsparkle-qt6.dll"
!else
File "Qt5Concurrent.dll"
@@ -248,24 +279,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 +372,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 +413,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 +443,28 @@ Section "Uninstall"
Delete "$INSTDIR\strawberry.ico"
Delete "$INSTDIR\strawberry.exe"
Delete "$INSTDIR\strawberry-tagreader.exe"
Delete "$INSTDIR\sqlite3.exe"
Delete "$INSTDIR\gst-launch-1.0.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 +504,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 +534,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 +541,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

@@ -107,10 +107,10 @@ void _MessageHandlerBase::WriteMessage(const QByteArray &data) {
else if (flush_local_socket_) {
((qobject_cast<QLocalSocket*>(device_))->*(flush_local_socket_))();
}
}
void _MessageHandlerBase::DeviceClosed() {
is_device_closed_ = true;
AbortAll();
}

View File

@@ -43,7 +43,7 @@ class QIODevice;
class _MessageHandlerBase : public QObject {
Q_OBJECT
public:
public:
// device can be nullptr, in which case you must call SetDevice before writing any messages.
_MessageHandlerBase(QIODevice *device, QObject *parent);
@@ -52,16 +52,16 @@ public:
// After this is true, messages cannot be sent to the handler any more.
bool is_device_closed() const { return is_device_closed_; }
protected slots:
protected slots:
void WriteMessage(const QByteArray &data);
void DeviceReadyRead();
virtual void DeviceClosed();
protected:
protected:
virtual bool RawMessageArrived(const QByteArray &data) = 0;
virtual void AbortAll() = 0;
protected:
protected:
typedef bool (QAbstractSocket::*FlushAbstractSocket)();
typedef bool (QLocalSocket::*FlushLocalSocket)();
@@ -80,7 +80,7 @@ protected:
// You should subclass this and implement the MessageArrived(MessageType) method.
template <typename MT>
class AbstractMessageHandler : public _MessageHandlerBase {
public:
public:
AbstractMessageHandler(QIODevice *device, QObject *parent);
~AbstractMessageHandler() override { AbortAll(); }
@@ -102,7 +102,7 @@ public:
// Sets the "id" field of reply to the same as the request, and sends the reply on the socket. Used on the worker side.
void SendReply(const MessageType &request, MessageType *reply);
protected:
protected:
// Called when a message is received from the socket.
virtual void MessageArrived(const MessageType &message) { Q_UNUSED(message); }
@@ -110,8 +110,8 @@ protected:
bool RawMessageArrived(const QByteArray &data) override;
void AbortAll() override;
private:
QMap<int, ReplyType*> pending_replies_;
private:
QMap<qint64, ReplyType*> pending_replies_;
};
template <typename MT>
@@ -146,6 +146,7 @@ void AbstractMessageHandler<MT>::SendReply(const MessageType &request, MessageTy
template<typename MT>
bool AbstractMessageHandler<MT>::RawMessageArrived(const QByteArray &data) {
MessageType message;
if (!message.ParseFromArray(data.constData(), data.size())) {
return false;
@@ -161,14 +162,17 @@ bool AbstractMessageHandler<MT>::RawMessageArrived(const QByteArray &data) {
}
return true;
}
template<typename MT>
void AbstractMessageHandler<MT>::AbortAll() {
for (ReplyType *reply : pending_replies_) {
reply->Abort();
}
pending_replies_.clear();
}
#endif // MESSAGEHANDLER_H

View File

@@ -20,6 +20,7 @@
#include <QtGlobal>
#include <QObject>
#include <QThread>
#include <QSemaphore>
#include <QString>
@@ -33,7 +34,7 @@ class _MessageReplyBase : public QObject {
public:
explicit _MessageReplyBase(QObject *parent = nullptr);
virtual int id() const = 0;
virtual qint64 id() const = 0;
bool is_finished() const { return finished_; }
bool is_successful() const { return success_; }
@@ -57,29 +58,27 @@ class _MessageReplyBase : public QObject {
template <typename MessageType>
class MessageReply : public _MessageReplyBase {
public:
explicit MessageReply(const MessageType& request_message, QObject *parent = nullptr);
explicit MessageReply(const MessageType &request_message, QObject *parent = nullptr);
int id() const override { return request_message_.id(); }
const MessageType& request_message() const { return request_message_; }
const MessageType& message() const { return reply_message_; }
qint64 id() const override { return request_message_.id(); }
const MessageType &request_message() const { return request_message_; }
const MessageType &message() const { return reply_message_; }
void SetReply(const MessageType& message);
void SetReply(const MessageType &message);
private:
private:
MessageType request_message_;
MessageType reply_message_;
};
template<typename MessageType>
MessageReply<MessageType>::MessageReply(const MessageType& request_message, QObject *parent)
: _MessageReplyBase(parent)
{
MessageReply<MessageType>::MessageReply(const MessageType &request_message, QObject *parent) : _MessageReplyBase(parent) {
request_message_.MergeFrom(request_message);
}
template<typename MessageType>
void MessageReply<MessageType>::SetReply(const MessageType& message) {
void MessageReply<MessageType>::SetReply(const MessageType &message) {
Q_ASSERT(!finished_);
@@ -90,6 +89,9 @@ void MessageReply<MessageType>::SetReply(const MessageType& message) {
qLog(Debug) << "Releasing ID" << id() << "(finished)";
semaphore_.release();
// The signal is not always emitted without this.
QThread::usleep(10);
emit Finished(success_);
}

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

@@ -32,7 +32,7 @@
#include <QQueue>
#include <QString>
#include <QStringList>
#include <QAtomicInt>
#include <QAtomicInteger>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#endif
@@ -48,11 +48,11 @@ class _WorkerPoolBase : public QObject {
public:
explicit _WorkerPoolBase(QObject *parent = nullptr);
signals:
signals:
// Emitted when a worker failed to start. This usually happens when the worker wasn't found, or couldn't be executed.
void WorkerFailedToStart();
protected slots:
protected slots:
virtual void DoStart() {}
virtual void NewConnection() {}
virtual void ProcessError(QProcess::ProcessError) {}
@@ -78,7 +78,7 @@ class WorkerPool : public _WorkerPoolBase {
void SetExecutableName(const QString &executable_name);
// Sets the number of worker process to use. Defaults to 1 <= (processors / 2) <= 2.
void SetWorkerCount(int count);
void SetWorkerCount(const int count);
// Sets the prefix to use for the local server (on unix this is a named pipe in /tmp).
// Defaults to QApplication::applicationName().
@@ -93,14 +93,14 @@ class WorkerPool : public _WorkerPoolBase {
// Can be called from any thread.
ReplyType *SendMessageWithReply(MessageType *message);
protected:
protected:
// These are all reimplemented slots, they are called on the WorkerPool's thread.
void DoStart() override;
void NewConnection() override;
void ProcessError(QProcess::ProcessError error) override;
void SendQueuedMessages() override;
private:
private:
struct Worker {
Worker() : local_server_(nullptr), local_socket_(nullptr), process_(nullptr), handler_(nullptr) {}
@@ -138,7 +138,7 @@ private:
// Returns the next handler, or nullptr if there isn't one. Must be called from my thread.
HandlerType *NextHandler() const;
private:
private:
QString local_server_name_;
QString executable_name_;
QString executable_path_;
@@ -147,7 +147,7 @@ private:
mutable int next_worker_;
QList<Worker> workers_;
QAtomicInt next_id_;
QAtomicInteger<qint64> next_id_;
QMutex message_queue_mutex_;
QQueue<ReplyType*> message_queue_;
@@ -170,6 +170,7 @@ WorkerPool<HandlerType>::WorkerPool(QObject *parent)
template <typename HandlerType>
WorkerPool<HandlerType>::~WorkerPool() {
for (const Worker &worker : workers_) {
if (worker.local_socket_ && worker.process_) {
disconnect(worker.process_, SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(ProcessError(QProcess::ProcessError)));
@@ -193,10 +194,11 @@ WorkerPool<HandlerType>::~WorkerPool() {
for (ReplyType *reply : message_queue_) {
reply->Abort();
}
}
template <typename HandlerType>
void WorkerPool<HandlerType>::SetWorkerCount(int count) {
void WorkerPool<HandlerType>::SetWorkerCount(const int count) {
Q_ASSERT(workers_.isEmpty());
worker_count_ = count;
}
@@ -220,6 +222,7 @@ void WorkerPool<HandlerType>::Start() {
template <typename HandlerType>
void WorkerPool<HandlerType>::DoStart() {
Q_ASSERT(workers_.isEmpty());
Q_ASSERT(!executable_name_.isEmpty());
Q_ASSERT(QThread::currentThread() == thread());
@@ -248,10 +251,12 @@ void WorkerPool<HandlerType>::DoStart() {
workers_ << worker;
}
}
template <typename HandlerType>
void WorkerPool<HandlerType>::StartOneWorker(Worker *worker) {
Q_ASSERT(QThread::currentThread() == thread());
DeleteQObjectPointerLater(&worker->local_server_);
@@ -284,6 +289,7 @@ void WorkerPool<HandlerType>::StartOneWorker(Worker *worker) {
// Start the process
worker->process_->setProcessChannelMode(QProcess::ForwardedChannels);
worker->process_->start(executable_path_, QStringList() << worker->local_server_->fullServerName());
}
template <typename HandlerType>
@@ -338,20 +344,24 @@ void WorkerPool<HandlerType>::ProcessError(QProcess::ProcessError error) {
StartOneWorker(worker);
break;
}
}
template <typename HandlerType>
typename WorkerPool<HandlerType>::ReplyType*
WorkerPool<HandlerType>::NewReply(MessageType *message) {
const int id = next_id_.fetchAndAddOrdered(1);
const qint64 id = next_id_.fetchAndAddOrdered(1);
message->set_id(id);
return new ReplyType(*message);
}
template <typename HandlerType>
typename WorkerPool<HandlerType>::ReplyType*
WorkerPool<HandlerType>::SendMessageWithReply(MessageType *message) {
ReplyType *reply = NewReply(message);
// Add the pending reply to the queue
@@ -364,10 +374,12 @@ WorkerPool<HandlerType>::SendMessageWithReply(MessageType *message) {
metaObject()->invokeMethod(this, "SendQueuedMessages", Qt::QueuedConnection);
return reply;
}
template <typename HandlerType>
void WorkerPool<HandlerType>::SendQueuedMessages() {
QMutexLocker l(&message_queue_mutex_);
while (!message_queue_.isEmpty()) {
@@ -384,10 +396,12 @@ void WorkerPool<HandlerType>::SendQueuedMessages() {
handler->SendRequest(reply);
}
}
template <typename HandlerType>
HandlerType *WorkerPool<HandlerType>::NextHandler() const {
for (int i = 0; i < workers_.count(); ++i) {
const int worker_index = (next_worker_ + i) % workers_.count();
@@ -398,6 +412,7 @@ HandlerType *WorkerPool<HandlerType>::NextHandler() const {
}
return nullptr;
}
#endif // WORKERPOOL_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,21 @@ 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 result = false;
if (TagLib::FLAC::File *file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
TagLib::Ogg::XiphComment *tag = file->xiphComment();
@@ -609,14 +565,14 @@ 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);
}
else if (TagLib::MP4::File *file_mp4 = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
@@ -635,53 +591,26 @@ bool TagReader::SaveFile(const QString &filename, const pb::tagreader::SongMetad
SetVorbisComments(tag, song);
}
bool ret = fileref->save();
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 +618,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 +632,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 +641,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 +652,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 +671,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 +776,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 +787,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 +797,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

@@ -101,7 +101,7 @@ message LoadEmbeddedArtResponse {
}
message Message {
optional int32 id = 1;
optional int64 id = 1;
optional ReadFileRequest read_file_request = 2;
optional ReadFileResponse read_file_response = 3;

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

@@ -27,11 +27,9 @@
#include "tagreaderworker.h"
TagReaderWorker::TagReaderWorker(QIODevice *socket, QObject *parent)
: AbstractMessageHandler<pb::tagreader::Message>(socket, parent)
{
}
: AbstractMessageHandler<pb::tagreader::Message>(socket, parent) {}
void TagReaderWorker::MessageArrived(const pb::tagreader::Message& message) {
void TagReaderWorker::MessageArrived(const pb::tagreader::Message &message) {
pb::tagreader::Message reply;
@@ -51,6 +49,7 @@ void TagReaderWorker::MessageArrived(const pb::tagreader::Message& message) {
}
SendReply(message, &reply);
}

View File

@@ -29,14 +29,14 @@
class QIODevice;
class TagReaderWorker : public AbstractMessageHandler<pb::tagreader::Message> {
public:
public:
explicit TagReaderWorker(QIODevice *socket, QObject *parent = nullptr);
protected:
protected:
void MessageArrived(const pb::tagreader::Message &message) override;
void DeviceClosed() override;
private:
private:
TagReader tag_reader_;
};

View File

@@ -1,10 +1,9 @@
name: strawberry
version: '0.7.1+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,12 +99,13 @@ parts:
- qtbase5-dev
- qtbase5-dev-tools
- qtbase5-private-dev
- qttools5-dev
- libqt5x11extras5-dev
- libgstreamer1.0-dev
- libgstreamer-plugins-base1.0-dev
- libvlc-dev
- libcdio-dev
- libgpod-devdev
- libgpod-dev
- libmtp-dev
- libchromaprint-dev
- libfftw3-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

@@ -19,6 +19,7 @@ set(SOURCES
core/multisortfilterproxy.cpp
core/musicstorage.cpp
core/network.cpp
core/threadsafenetworkdiskcache.cpp
core/networktimeouts.cpp
core/networkproxyfactory.cpp
core/qtfslistener.cpp
@@ -33,12 +34,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 +95,7 @@ set(SOURCES
playlist/playlistview.cpp
playlist/songloaderinserter.cpp
playlist/songplaylistitem.cpp
playlist/dynamicplaylistcontrols.cpp
queue/queue.cpp
queue/queueview.cpp
@@ -111,6 +111,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 +183,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 +209,7 @@ set(SOURCES
widgets/tracksliderpopup.cpp
widgets/tracksliderslider.cpp
widgets/loginstatewidget.cpp
widgets/ratingwidget.cpp
osd/osdbase.cpp
osd/osdpretty.cpp
@@ -222,6 +240,7 @@ set(SOURCES
scrobbler/lastfmscrobbler.cpp
scrobbler/librefmscrobbler.cpp
scrobbler/listenbrainzscrobbler.cpp
scrobbler/lastfmimport.cpp
organize/organize.cpp
organize/organizeformat.cpp
@@ -239,6 +258,7 @@ set(HEADERS
core/filesystemwatcherinterface.h
core/mergedproxymodel.h
core/network.h
core/threadsafenetworkdiskcache.h
core/networktimeouts.h
core/qtfslistener.h
core/songloader.h
@@ -297,6 +317,7 @@ set(HEADERS
playlist/playlistitemmimedata.h
playlist/songloaderinserter.h
playlist/songmimedata.h
playlist/dynamicplaylistcontrols.h
queue/queue.h
queue/queueview.h
@@ -310,6 +331,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 +400,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 +426,7 @@ set(HEADERS
widgets/tracksliderslider.h
widgets/loginstatewidget.h
widgets/qsearchfield.h
widgets/ratingwidget.h
osd/osdbase.h
osd/osdpretty.h
@@ -418,6 +455,7 @@ set(HEADERS
scrobbler/lastfmscrobbler.h
scrobbler/librefmscrobbler.h
scrobbler/listenbrainzscrobbler.h
scrobbler/lastfmimport.h
organize/organize.h
organize/organizedialog.h
@@ -438,9 +476,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 +516,8 @@ set(UI
dialogs/trackselectiondialog.ui
dialogs/addstreamdialog.ui
dialogs/userpassdialog.ui
dialogs/lastfmimportdialog.ui
dialogs/snapdialog.ui
widgets/trackslider.ui
widgets/fileview.ui
@@ -502,19 +550,19 @@ if(HAVE_GLOBALSHORTCUTS)
HEADERS globalshortcuts/globalshortcuts.h globalshortcuts/globalshortcutbackend.h globalshortcuts/globalshortcutgrabber.h settings/shortcutssettingspage.h
UI globalshortcuts/globalshortcutgrabber.ui settings/shortcutssettingspage.ui
)
if (X11_FOUND OR WIN32)
if(HAVE_X11EXTRAS OR WIN32)
set(X11_OR_WIN ON)
endif()
optional_source(X11_OR_WIN
SOURCES globalshortcuts/globalshortcutbackend-system.cpp globalshortcuts/globalshortcut.cpp
HEADERS globalshortcuts/globalshortcutbackend-system.h globalshortcuts/globalshortcut.h
)
optional_source(X11_FOUND
optional_source(HAVE_X11EXTRAS
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 +600,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 +608,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 +636,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 +672,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 +721,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 +881,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 +919,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 +957,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 +1003,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 +1051,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 +1101,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 +1125,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 +1151,7 @@ target_link_libraries(strawberry_lib PUBLIC
${SINGLEAPPLICATION_LIBRARIES}
${SINGLECOREAPPLICATION_LIBRARIES}
${QTSPARKLE_LIBRARIES}
${Iconv_LIBRARY}
libstrawberry-common
libstrawberry-tagreader
)
@@ -1131,8 +1220,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)
@@ -1208,7 +1297,7 @@ if(NOT APPLE)
endif()
if(HAVE_TRANSLATIONS AND INSTALL_TRANSLATIONS AND INSTALL_TRANSLATIONS_FILES)
install(FILES ${INSTALL_TRANSLATIONS_FILES} DESTINATION share/translations)
install(FILES ${INSTALL_TRANSLATIONS_FILES} DESTINATION share/strawberry/translations)
endif()
if(APPLE)

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, qint64)), backend_, SLOT(UpdateLastPlayed(QString, QString, QString, qint64)));
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 qint64 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 qint64 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

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2019, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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

@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -70,6 +71,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

@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

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

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