Compare commits

..

350 Commits

Author SHA1 Message Date
Jonas Kvinge
c028770f8e Release 1.0.18 2023-07-02 01:27:51 +02:00
Jonas Kvinge
8f1a99b37e CI: Remove Fedora 36 2023-07-02 01:02:10 +02:00
Jonas Kvinge
e66651a4cb Song: Remove unused application include 2023-07-02 00:29:30 +02:00
Jonas Kvinge
1d9a052870 AlbumCoverChoiceController: Move result declaration 2023-07-02 00:29:19 +02:00
Jonas Kvinge
d3ee749c14 CollectionBackendTest: Fix build 2023-07-01 22:34:51 +02:00
Jonas Kvinge
33076aa33a Update Changelog 2023-07-01 22:26:52 +02:00
Jonas Kvinge
2a2663eeb5 Update Changelog 2023-06-29 20:31:16 +02:00
Jonas Kvinge
fa04eb67db Convert old embedded and unset art in the new schema 2023-06-29 20:21:50 +02:00
Jonas Kvinge
f8ad8a7211 Disable clang-format 2023-06-29 19:06:27 +02:00
Jonas Kvinge
5f4d6dffef AlbumCoverLoader: Fix loading existing album covers from disk 2023-06-29 00:44:00 +02:00
Jonas Kvinge
b9c7510946 AlbumCoverLoader: Fix Handling default image in finish task 2023-06-28 23:52:39 +02:00
dependabot[bot]
c0e709a0e3 Bump vmactions/freebsd-vm from 0.3.0 to 0.3.1
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 0.3.0 to 0.3.1.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v0.3.0...v0.3.1)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 20:16:20 +02:00
Jonas Kvinge
e690be1bdd DeviceDatabaseBackend: Fix device schema version mismatch
schema_version in device-schema.sql is 4.
2023-06-26 20:06:11 +02:00
Jonas Kvinge
6ed5190276 Remove AudD lyrics 2023-06-25 01:14:12 +02:00
Jonas Kvinge
4c4a351fbd CI: Add ubuntu mantic for ppa 2023-06-25 00:28:21 +02:00
Jonas Kvinge
435ffc75b6 CI: Add debian trixie 2023-06-24 03:40:23 +02:00
Jonas Kvinge
4dbc06bdd0 CI: Add ubuntu mantic 2023-06-24 03:38:32 +02:00
Jonas Kvinge
5a7cbb2f3d CI: Add OpenMandriva Cooker, remove Lx 4.2 2023-06-24 00:56:53 +02:00
Jonas Kvinge
354b55cbbc Use QFileInfo::path instead of QUrl::RemoveFilename 2023-06-15 21:06:18 +02:00
Jonas Kvinge
b87a950357 CoverUtils: Only create path if it doesn't exist 2023-06-15 21:04:11 +02:00
Jonas Kvinge
950c236720 AlbumCoverManager: Fix clear and unset actions 2023-06-15 20:35:41 +02:00
Jonas Kvinge
32982be4f2 EditTagDialog: Allow clearing unset cover 2023-06-15 20:34:03 +02:00
Jonas Kvinge
f467331934 Add ifdefs around #pragma GCC diagnostic 2023-06-15 20:10:25 +02:00
Jonas Kvinge
ec839e6aae KDSingleApplicationLocalSocket: Ignore missing declaration 2023-06-15 19:48:00 +02:00
Jonas Kvinge
45f1521da6 Update Changelog 2023-06-14 23:10:56 +02:00
Jonas Kvinge
841a44a18e KDSingleApplicationLocalSocket: Remove extra semicolon 2023-06-14 23:10:50 +02:00
Dakes
1deacaecf9 FilterParser: Add ability to filter by rating
Playlists can now be filtered by the rating from 0-5 like:
rating:0 rating:<3 rating:!=0
or by a float value, like:
rating:f0.1 rating:>=f0.5
2023-06-09 21:31:15 +02:00
Jonas Kvinge
6af8e6c25b Update Changelog 2023-06-09 00:16:46 +02:00
Jonas Kvinge
b0849d21f3 TidalStreamURLRequest: Ignore different track ID 2023-06-08 17:36:24 +02:00
Jonas Kvinge
5b60ea8e77 MainWindow: Remove F2 from edit tag shortcut
Fixes #1210
2023-06-07 19:38:59 +02:00
Andrei Stepanov
ec3f95a260 Add Russian translation to desktop file 2023-06-07 17:41:17 +02:00
Jonas Kvinge
0bfd4d2e04 CI: Only run SSH key setup step for master branch 2023-06-07 16:57:27 +02:00
Jonas Kvinge
a6ba0cfc97 KDSingleApplicationLocalSocket: Exclude XDG_SESSION_ID from socket name 2023-06-07 01:23:01 +02:00
Jonas Kvinge
6d55eb5974 CollectionModel: Fix icon disk cache 2023-06-07 00:51:53 +02:00
Jonas Kvinge
972053c699 MoodbarLoader: Fix loading cached moodbars 2023-06-07 00:50:35 +02:00
Jonas Kvinge
9db7896828 Prefix class for ReloadSettings 2023-06-06 23:20:42 +02:00
Jonas Kvinge
be6f93735d Simplify if statements 2023-06-06 23:19:45 +02:00
Jonas Kvinge
3bcea249ac TagReaderTagLib: Initialize cover_format 2023-06-06 23:19:25 +02:00
Jonas Kvinge
8ee32dfa88 Add static_cast to silence narrowing conversion warnings 2023-06-06 23:18:49 +02:00
Jonas Kvinge
e2f9411b46 EditTagDialog: Initialize cover_action 2023-06-06 23:17:29 +02:00
Jonas Kvinge
0bd68c3817 KDSingleApplicationLocalSocket: Add static_cast to silence narrowing conversion warnings 2023-06-06 22:25:19 +02:00
Jonas Kvinge
f03505ab67 KDSingleApplicationLocalSocket: Remove useless std::move 2023-06-06 22:24:10 +02:00
Jonas Kvinge
f1ae795c0f KDSingleApplicationLocalSocket: Initialize socket 2023-06-06 22:23:54 +02:00
Jonas Kvinge
50fcda2763 OSDDBus: Remove use of QImage::isGrayscale, Always expect RGB
Fixes #1205
2023-06-06 21:43:05 +02:00
Jonas Kvinge
331aa382f9 Rewrite album cover loader 2023-06-06 20:41:01 +02:00
Jonas Kvinge
3c160c2f13 CI: Fix /etc/dnf/dnf.conf for Fedora 39 2023-06-06 00:09:48 +02:00
Jonas Kvinge
4fd617a32f nsi: Add libgcc_s_sjlj-1.dll and libwinpthread-1.dll for MSVC x86 2023-06-05 00:41:34 +02:00
Jonas Kvinge
0d294eb218 CI: Add /opt/local/bin to PATH for macports build 2023-06-05 00:35:11 +02:00
Jonas Kvinge
a49e3b90b0 CI: Use macOS 12 for FreeBSD vm 2023-06-03 19:19:57 +02:00
Jonas Kvinge
201392f22e CI: Use latest macports release 2023-06-03 19:11:18 +02:00
Jonas Kvinge
3fa4ee772f CI: Bump MACOSX_DEPLOYMENT_TARGET 2023-06-03 19:10:57 +02:00
Jonas Kvinge
529c83c572 CI: Bump macOS version 2023-06-03 15:50:53 +02:00
Jonas Kvinge
6a9e56e9d9 nsi: Add gstwinrt-1.0-0.dll 2023-06-01 21:37:18 +02:00
Jonas Kvinge
716e80fb84 UWPDeviceFinder: Fix EngineDeviceList 2023-06-01 21:35:48 +02:00
Jonas Kvinge
80067b806d TranscodeDialog: Append number to filename if it already exists
Fixes #1200
2023-06-01 20:42:47 +02:00
Jonas Kvinge
315073f9a7 Add EngineDevice class 2023-06-01 19:31:19 +02:00
Jonas Kvinge
a1dbbba1a1 ContextView: Remove engine and device 2023-06-01 19:29:52 +02:00
Jonas Kvinge
cb8a0b6853 ContextSettingsPage: Remove engine and device 2023-06-01 19:29:52 +02:00
Jonas Kvinge
a5a29f7ad3 DeviceFinder: Add typedef DeviceList 2023-06-01 18:43:43 +02:00
Jonas Kvinge
8e14ef7c0c GstStartup: Set rank for directsoundsink higher than wasapisink for MinGW
Fixes #1204
2023-06-01 18:31:55 +02:00
Jonas Kvinge
c270391772 Add WASAPI2 plugin
Fixes #1204
2023-06-01 18:16:51 +02:00
Jonas Kvinge
e466cb6e30 Add UWP device finder 2023-06-01 18:11:30 +02:00
Jonas Kvinge
f0df9dc0fb GstEngine: Append "2" to wasapi2sink description 2023-06-01 17:22:11 +02:00
Strawbs Bot
b31c08083a Update translations 2023-05-30 01:02:07 +02:00
Jonas Kvinge
145a8d5b67 Bump LSMinimumSystemVersion in Info.plist
Fixes #1206
2023-05-29 11:25:43 +02:00
Jonas Kvinge
60d7a4e7ee AlbumCoverManager: Fix invalid reference 2023-05-29 11:24:41 +02:00
Jonas Kvinge
b52ffd09b2 Use find_package Protobuf CONFIG 2023-05-14 23:54:36 +02:00
Jonas Kvinge
58278ab1e4 Replace protobuf_generate_cpp with protobuf_generate 2023-05-14 20:07:36 +02:00
Strawbs Bot
000cf5fd5a Update translations 2023-05-13 01:01:35 +02:00
Jonas Kvinge
216b58e392 CI: Upload *.xz to source path 2023-05-07 12:39:35 +02:00
Jonas Kvinge
279cce49ba CI: Fix MSVC tarball filename 2023-05-07 11:27:56 +02:00
Jonas Kvinge
8184b77b13 CI: Improve MSVC build 2023-05-07 05:25:47 +02:00
Jonas Kvinge
b8b731ab04 CI: Upload Ubuntu PPA 2023-05-07 04:28:56 +02:00
Jonas Kvinge
1d4c39d13d CI: Upload release 2023-05-07 02:24:39 +02:00
Jonas Kvinge
ac26f5b2ef Add git to debian/control 2023-05-07 02:24:24 +02:00
Jonas Kvinge
c07447d8f5 CI: Upload artifacts 2023-05-07 01:09:18 +02:00
Jonas Kvinge
833ddf5f4c CI: Use -S and -B for cmake command 2023-05-06 14:18:35 +02:00
Jonas Kvinge
4dc4f468bb CI: Add Mageia 2023-05-06 14:08:34 +02:00
Strawbs Bot
1aff69d3cf Update translations 2023-05-06 01:02:19 +02:00
Robert Marshall
f2f63a703e class CueParser Process composer metadata in cue files
Use composer as an alternative to songwriter

..and move a misplaced comment
2023-05-05 17:47:37 +02:00
Jonas Kvinge
97e6b17f96 ContextView: QFontDatabase::families is not static in Qt 5 2023-05-05 17:46:02 +02:00
Jonas Kvinge
b90d284b08 ContextView: Check for default font family 2023-05-05 16:43:34 +02:00
Jonas Kvinge
840a65c630 ContextSettingsPage: Use constant for default font family 2023-05-05 16:43:01 +02:00
Jonas Kvinge
e5c89d4881 KDSingleApplicationLocalSocket: Use namespace and constexpr 2023-05-05 16:42:12 +02:00
Jonas Kvinge
0fc6638294 KDSingleApplicationLocalSocket: Formatting 2023-05-05 16:41:50 +02:00
Strawbs Bot
7c7724e41c Update translations 2023-05-05 01:29:26 +02:00
Jonas Kvinge
31319711dd Remove fixed font size on macOS
Possible fix for #1184
2023-05-05 00:34:48 +02:00
Jonas Kvinge
8954729d55 KDSingleApplicationPrivate: Remove unused initialize declaration 2023-05-04 19:42:27 +02:00
Jonas Kvinge
ae4dc442b9 Update Changelog 2023-05-04 19:22:00 +02:00
Jonas Kvinge
919ff414e6 Replace SingleApplication with KDSingleApplication 2023-05-04 09:44:54 +02:00
Strawbs Bot
b861703dad Update translations 2023-05-04 01:15:19 +02:00
Jonas Kvinge
2f17647cd3 Use const reference for AlbumCoverLoaderResult 2023-05-03 21:43:22 +02:00
Jonas Kvinge
f8d2c7eba3 Bump required Qt version to 5.12 2023-05-03 20:50:58 +02:00
Jonas Kvinge
e511b2faf9 Use new connect syntax for QMetaObject::invokeMethod 2023-05-03 20:08:51 +02:00
Jonas Kvinge
301e6b194a Replace 0 msec singleshot with invoke method 2023-05-03 19:55:58 +02:00
Jonas Kvinge
1208ca3ad4 ContextView: Simplify font code 2023-05-03 01:59:13 +02:00
Jonas Kvinge
84e7cd0df8 CollectionWatcher: Connect PathChanged signal once 2023-05-03 01:17:10 +02:00
Strawbs Bot
7e1077426e Update translations 2023-05-02 01:01:40 +02:00
Jonas Kvinge
1e2c437a08 CMakeLists: Remove hardcoded sqlite3 link with MSVC 2023-05-01 19:20:00 +02:00
Jonas Kvinge
a7ce1d1225 CI: Strip gdb.exe 2023-05-01 17:26:12 +02:00
Jonas Kvinge
1d3223e9c6 Add getopt supporting unicode
Fixes #1191
2023-05-01 16:44:18 +02:00
Jonas Kvinge
b01f3f4bb5 CI: Build on macOS with macports 2023-04-29 21:11:05 +02:00
Strawbs Bot
ee4cd11ae1 Update translations 2023-04-29 01:01:40 +02:00
Jonas Kvinge
154f8b307e CI: Add openSUSE Leap 15.5 2023-04-28 21:28:24 +02:00
Jonas Kvinge
7aac7872e3 CI: Disable D-Bus on macOS
Fixes #1163
2023-04-28 16:59:28 +02:00
Jonas Kvinge
b2630032e7 CI: Remove CodeQL 2023-04-27 21:39:25 +02:00
Jonas Kvinge
80136b602b CI: Check that all files are included in nsi 2023-04-26 19:04:59 +02:00
Strawbs Bot
3df29ed2a9 Update translations 2023-04-25 01:01:22 +02:00
Jonas Kvinge
69658f2022 AlbumCoverChoiceController: Check for nullptr in SearchForCover 2023-04-23 19:27:01 +02:00
Jonas Kvinge
5fd0a0831f GstEngine: Include all outputs that starts with "Sink/Audio" 2023-04-23 01:28:52 +02:00
Strawbs Bot
43b299ebd1 Update translations 2023-04-23 01:21:40 +02:00
Jonas Kvinge
9612304023 GstEngine: Use _stricmp with MSVC 2023-04-23 01:20:11 +02:00
Jonas Kvinge
3154e59b36 EngineBase: Remove extra semicolon 2023-04-22 20:41:22 +02:00
Jonas Kvinge
2d5a496678 Update debian/copyright 2023-04-22 19:48:51 +02:00
Jonas Kvinge
4c1c322b54 Rename AnalyzerBase 2023-04-22 19:45:21 +02:00
Jonas Kvinge
e9f3281694 Rename EngineBase 2023-04-22 19:13:42 +02:00
Jonas Kvinge
c3534affdb EngineBase: Remove PluginDetails 2023-04-22 17:18:29 +02:00
Jonas Kvinge
c6e62b3263 GstStartup: Remove setting rank for wasapisink and directsoundsink 2023-04-22 17:15:41 +02:00
Jonas Kvinge
d8ce177ce7 GstEngine: Remove class comment 2023-04-22 17:14:49 +02:00
Jonas Kvinge
726bfbefb0 Replace gst_element_factory_get_klass with gst_element_factory_get_metadata 2023-04-22 16:49:37 +02:00
Jonas Kvinge
5c4a6487c0 Update Changelog 2023-04-22 15:27:46 +02:00
Jonas Kvinge
3145433099 SystemTrayIcon: Simply icon loading 2023-04-22 15:27:34 +02:00
Jonas Kvinge
bdf6844b74 Database: Move error_reported 2023-04-22 14:38:45 +02:00
Jonas Kvinge
9ad430915c CddaDevice: Remove application.h include 2023-04-22 14:31:25 +02:00
Jonas Kvinge
60bbd71a22 Remove NetworkAccessManager from song loader 2023-04-22 14:05:48 +02:00
Jonas Kvinge
c96498758f Fix and improve gapless playback
If "about-to-finish" was emitted before the preload time was reached, we never set the next uri, so gapless playback was broken.
Make sure to always set the next uri, and increase preload gap from 5 to 8 seconds.
2023-04-22 03:54:11 +02:00
Strawbs Bot
f4600bd8eb Update translations 2023-04-22 01:21:46 +02:00
Jonas Kvinge
7202a5734c ListenBrainzScrobbler: Only send duration_ms when valid 2023-04-21 20:54:18 +02:00
Jonas Kvinge
8edb6eaa81 Song: Add missing const 2023-04-21 20:48:30 +02:00
Jonas Kvinge
b8eecc05fd AlbumCoverLoader: Use own NetworkAccessManager instance
Since AlbumCoverLoader runs in it's own thread.
2023-04-21 20:29:43 +02:00
Jonas Kvinge
7fc5aef553 Use one instance of NetworkAccessManager 2023-04-21 20:20:53 +02:00
Jonas Kvinge
bee6b7f946 Rename original_url to media_url 2023-04-21 16:20:00 +02:00
Jonas Kvinge
3bedfb6ac8 GstEngine: Formatting 2023-04-21 15:07:17 +02:00
Jonas Kvinge
f49bf0192b Engine::Base: Formatting 2023-04-21 15:06:44 +02:00
Jonas Kvinge
f36ac5272b Scrobbler: Simplify error handling 2023-04-21 02:11:26 +02:00
Strawbs Bot
f0fe446f7f Update translations 2023-04-21 01:01:44 +02:00
Jonas Kvinge
d9c4720a3e ListenBrainzScrobbler: Split artist mbids by slash 2023-04-20 21:55:11 +02:00
monochromec
4de5d8f2a3 Fixed typo and improved wording of Rosetta notice 2023-04-20 16:29:48 +02:00
Strawbs Bot
8651311016 Update translations 2023-04-19 01:08:38 +02:00
Jonas Kvinge
1d27c5a14c MainWindow: Add missing QSettings declaration 2023-04-18 20:17:01 +02:00
Jonas Kvinge
b0b8ff2d49 CollectionModel: URL percent encode disk cache keys
Fixes #1183
2023-04-18 18:42:37 +02:00
Jonas Kvinge
cd03e1fc74 Make it possible to not show Rosetta warning
Fixes #1180
2023-04-18 18:03:39 +02:00
Jonas Kvinge
b273a449e3 Add common message dialog 2023-04-18 17:44:42 +02:00
Jonas Kvinge
b637867f9e MainWindow: Hide checkbox, set size, center rosetta warning 2023-04-18 16:54:43 +02:00
Jonas Kvinge
41d5792b27 Add screen utilities for screen and center widget on screen 2023-04-18 16:54:35 +02:00
Jonas Kvinge
d22712a25b nsi: Update ICU to 73 2023-04-15 17:50:57 +02:00
Jonas Kvinge
33968ee5da CI: Fix macOS build 2023-04-15 17:50:31 +02:00
Strawbs Bot
9baf1774e0 Update translations 2023-04-15 01:01:23 +02:00
Strawbs Bot
9ca66e4061 Update translations 2023-04-12 01:01:23 +02:00
Jonas Kvinge
db2d34f840 FavoriteWidget: Use icon loader for star icons
Fixes #1178
2023-04-10 21:29:52 +02:00
Strawbs Bot
195cc61df7 Update translations 2023-04-10 01:22:37 +02:00
Jonas Kvinge
aaa530e72b Add const/references to all signal parameters 2023-04-09 20:23:42 +02:00
Jonas Kvinge
fa856ee905 Remove directory.h 2023-04-09 20:17:45 +02:00
Strawbs Bot
2bf7a5c4d3 Update translations 2023-04-08 01:27:44 +02:00
Jonas Kvinge
780b982635 SmartPlaylistSearchTermWidget: Fix loading search terms
Fixes #1172
2023-04-07 23:05:07 +02:00
Jonas Kvinge
74bbc1f19f PlaylistGenerator: Use std::make_shared 2023-04-07 22:41:10 +02:00
Jonas Kvinge
e314545f2e Database: Fix SQL query error message 2023-04-07 20:54:31 +02:00
Jonas Kvinge
f1a3a12c1c CollectionBackend: Fix SQL query error message 2023-04-07 20:54:15 +02:00
Jonas Kvinge
ef080c3cb1 PlaylistView: Fix resetting album cover 2023-04-07 20:07:40 +02:00
Jonas Kvinge
7313db5ac0 Add compile test for QX11Application
if Qt was compiled without XCB, globalshortcut-x11 failed to compile.
2023-04-07 15:57:39 +02:00
Jonas Kvinge
bf9aa524ed CI: Fix macOS build 2023-04-07 02:31:59 +02:00
Jonas Kvinge
b59aa0827e GstEnginePipeline: Always set initial volume 2023-04-07 02:30:41 +02:00
Strawbs Bot
2584f1293e Update translations 2023-04-07 01:13:27 +02:00
Jonas Kvinge
de9d28adea CI: Add Fedora 39 2023-04-06 23:37:23 +02:00
Jonas Kvinge
b660287779 Use std::shared_ptrfor AlbumCoverLoaderResult
Reduces memory fragmentation with Qt 6
2023-04-06 23:18:10 +02:00
Jonas Kvinge
962536bc83 AlbumCoverLoaderResult: Use enum class for type 2023-04-06 01:23:42 +02:00
Strawbs Bot
ee4fcf8100 Update translations 2023-04-04 01:01:21 +02:00
Strawbs Bot
9a4ed1a19d Update translations 2023-03-31 21:56:09 +02:00
Gaganpreet Arora
c1d0702b4d QobuzRequest: Handle multiple discs in integration
Albums with multiple discs are not being handled which results in tracks
being listed out of order. If the disc (media_number) is present in
JSON, we use it to set the disc in the Song object.
2023-03-31 21:21:58 +02:00
Jonas Kvinge
af1c46543b Turn on git revision 2023-03-30 17:50:03 +02:00
Jonas Kvinge
e2f5486987 Release 1.0.17 2023-03-29 18:54:51 +02:00
Jonas Kvinge
b0d390aaf1 Update Changelog 2023-03-29 18:49:35 +02:00
Jonas Kvinge
bae1b42394 OSDPretty: Respect device pixel ratio 2023-03-29 18:23:06 +02:00
Jonas Kvinge
a2533edd57 PlayingWidget: Make previous pixmap respect device pixel ratio 2023-03-29 00:24:38 +02:00
Jonas Kvinge
7b88c198fe SongSourceDelegate: Fix compile with Qt 5 2023-03-29 00:14:21 +02:00
Jonas Kvinge
c49cb0c119 ImageUtils: Formatting 2023-03-29 00:09:40 +02:00
Jonas Kvinge
03aabeb848 AlbumCoverManager: Respect device pixel ratio 2023-03-29 00:06:56 +02:00
Jonas Kvinge
1d3a837f7a SongSourceDelegate: Respect device pixel ratio 2023-03-29 00:04:34 +02:00
Jonas Kvinge
92adc18b8f ContextAlbum: Make size hint respect device pixel ratio 2023-03-28 18:21:19 +02:00
Fletcher Dostie
e4697c8ff1 Render context art at correct size 2023-03-28 17:54:56 +02:00
Jonas Kvinge
b741f1a580 Turn on git revision 2023-03-28 01:48:09 +02:00
Jonas Kvinge
8ee6de4162 Release 1.0.16 2023-03-27 22:28:02 +02:00
Jonas Kvinge
2b9e7db924 TagReaderTagLib: Touch file over saving cover art 2023-03-26 22:37:21 +02:00
Jonas Kvinge
49384ce294 TagReaderTagLib: Fix reading MusicBrainz from ID3v2 tags 2023-03-26 04:33:15 +02:00
Jonas Kvinge
ac59fff346 TagReaderTagLib: Formatting 2023-03-26 04:14:03 +02:00
Jonas Kvinge
a3e9f152d8 CollectionSettingsPage: Remove iopriority hide 2023-03-26 03:14:52 +02:00
Jonas Kvinge
ffe6a81b9a Collection: Always use Idle priority for watcher 2023-03-26 00:10:29 +01:00
Jonas Kvinge
9bb051b4eb CollectionSettingsPage: Remove settings for IO and thread priority and workers 2023-03-26 00:09:41 +01:00
Jonas Kvinge
c1465a890f TagReaderClient: Always use 1 tagreader worker 2023-03-26 00:08:55 +01:00
Jonas Kvinge
c3ee3318d7 WorkerPool: Default to 1 worker 2023-03-26 00:08:23 +01:00
Jonas Kvinge
272976f0b7 TagReaderClient: Fix default save options 2023-03-25 23:54:15 +01:00
Jonas Kvinge
4724b170b1 AuddLyricsProvider: Disable provider by default
Since we are currently missing API key.
2023-03-25 19:20:10 +01:00
Jonas Kvinge
337bd4bcef ListenBrainzScrobbler: Make sure we only add the same artist id once 2023-03-25 19:14:26 +01:00
Jonas Kvinge
0604c78453 CollectionBackend: Check for changed fingerprint 2023-03-25 18:35:14 +01:00
Jonas Kvinge
c8169adf7c CollectionModel: Guard against invalid disc and year 2023-03-25 18:32:41 +01:00
Jonas Kvinge
7b610d131c CollectionModel: Replace qMax with std::max 2023-03-25 18:31:21 +01:00
Jonas Kvinge
18da55453f EditTagDialog: Not set for track, disc, year, etc to -1 2023-03-25 18:29:12 +01:00
Jonas Kvinge
2aeab8b672 CollectionModel: Remove unused QByteArray include 2023-03-25 18:26:53 +01:00
Jonas Kvinge
9c21707a55 Update Changelog 2023-03-25 16:49:19 +01:00
Jonas Kvinge
b02ac833ad CollectionWatcher: Fix rescan songs feature 2023-03-25 16:38:03 +01:00
Jonas Kvinge
4de912cf41 Song: Fix missing colon in SQL bind value 2023-03-25 16:34:26 +01:00
Jonas Kvinge
ed260c6e20 CollectionWatcher: Check for changed AcoustID and MusicBrainz 2023-03-25 14:35:12 +01:00
Jonas Kvinge
fab38f693d Scrobbler: Refactor and add MusicBrainz integration 2023-03-25 14:25:21 +01:00
Jonas Kvinge
aedbd52e9d Add AcoustID 2023-03-24 22:48:22 +01:00
Jonas Kvinge
4cbcb9d99c Update list of lyrics providers in README.md and dist files 2023-03-24 21:23:10 +01:00
Jonas Kvinge
e967d15b4e Add AudD lyrics provider 2023-03-24 21:16:11 +01:00
Fletcher Dostie
bb43cc63ec Fix album art rendering on High DPI displays 2023-03-24 21:02:55 +01:00
Jonas Kvinge
ca176c319d Add Acoustid and MusicBrainz tags to database schema 2023-03-24 21:00:58 +01:00
Jonas Kvinge
17c960ecd4 Song: Add Acoustid fingerprint and MusicBrainz tags 2023-03-24 20:57:46 +01:00
Jonas Kvinge
b766ae3498 Song: Use static_cast from int to Source 2023-03-24 20:54:42 +01:00
Jonas Kvinge
faf4c817cd TagReaderTagLib: Read AcoustID fingerprint 2023-03-24 20:54:05 +01:00
Jonas Kvinge
b4e1f283c9 tagreadermessages: Add AcoustID fingerprint tags to protobuf 2023-03-24 19:52:45 +01:00
Jonas Kvinge
52c83d592c Rename Stands4 to Lyrics.com 2023-03-22 23:36:15 +01:00
Jonas Kvinge
c8caea0d30 GstEnginePipeline: Use constexpr 2023-03-22 23:29:56 +01:00
Jonas Kvinge
7a6c54d8e7 GstEnginePipeline: Remove hard-coded num-bands 2023-03-22 23:29:24 +01:00
Strawbs Bot
7082e52a4f Update translations 2023-03-21 01:01:49 +01:00
Strawbs Bot
6cdca617e0 Update translations 2023-03-20 01:14:38 +01:00
Jonas Kvinge
a1adc1a75a Add option for strict SSL mode in backend settings 2023-03-19 23:02:17 +01:00
Jonas Kvinge
b16bec704a GstEnginePipeline: Use "source-setup" instead of "notify::source" signal
This works with both playbin 2 and 3.

Possible fix for #1148
2023-03-19 22:28:12 +01:00
Jonas Kvinge
664a8c79a1 GstEnginePipeline: Check for ssl-strict property 2023-03-19 22:13:16 +01:00
Jonas Kvinge
bbf3d8e1a4 Update Changelog 2023-03-19 22:03:55 +01:00
Jonas Kvinge
a35146440c TagReaderTagLib: Read MusicBrainz tags 2023-03-19 21:55:26 +01:00
Jonas Kvinge
8e79fafca9 tagreadermessages: Add MusicBrainz tags to protobuf 2023-03-19 21:50:54 +01:00
Jonas Kvinge
6f8780d3cc TagReaderGME: Add header guard comment 2023-03-19 21:29:45 +01:00
Jonas Kvinge
264065a355 TagreaderGME: Don't call QByteArray::operator[]() on temporary 2023-03-19 20:42:30 +01:00
Jonas Kvinge
b606e4cd1a TagReaderTagLib: Rename Decode to TStringToStdString 2023-03-19 20:35:47 +01:00
Jonas Kvinge
01d0eeaed0 FileViewList: Add reserve 2023-03-19 20:32:06 +01:00
Jonas Kvinge
8c4e24e65d filemanagerutils: Add namespace comment 2023-03-19 20:31:46 +01:00
Jonas Kvinge
19c9f9698d QobuzService: Make DecodeAppSecret const 2023-03-19 20:31:31 +01:00
Jonas Kvinge
ae87c1b578 Stands4LyricsProvider: Remove unused variable 2023-03-19 20:24:07 +01:00
Jonas Kvinge
c42b1f5548 GstEnginePipeline: Cast guint to int 2023-03-19 19:38:36 +01:00
Jonas Kvinge
87ffbb0a85 Equalizer: Remove redundant QString cast 2023-03-19 19:38:12 +01:00
Jonas Kvinge
702851a958 TagReaderTagLib: Use constexpr 2023-03-19 19:34:05 +01:00
Jonas Kvinge
650f200a0b TagReaderBase: Remove unused Decode function 2023-03-19 19:33:34 +01:00
Jonas Kvinge
14d215bdf3 ContextView: Add static_cast for font size 2023-03-19 19:32:41 +01:00
Jonas Kvinge
12aebc2fe9 Sonogram: Remove unused destructor 2023-03-19 19:32:23 +01:00
Jonas Kvinge
f41b051ec7 LyricsSearchResult: Add missing const reference 2023-03-19 19:31:43 +01:00
Jonas Kvinge
25144eee89 Add CONTRIBUTING.md 2023-03-19 16:07:12 +01:00
Jonas Kvinge
0c62147536 SongLoader: Remove probe and bus watch 2023-03-19 05:08:35 +01:00
Jonas Kvinge
7b282e21de Fix compile with GStreamer disabled 2023-03-19 05:08:35 +01:00
Strawbs Bot
c1fbe6d84c Update translations 2023-03-19 01:20:59 +01:00
Jonas Kvinge
e20cbe4170 Save embedded cover in the same process as tags
Possible fix for #1158
2023-03-18 20:03:07 +01:00
Jonas Kvinge
394955a03f Replace QList<QByteArray> with QByteArrayList 2023-03-18 19:41:36 +01:00
Jonas Kvinge
16b4f5d065 DeviceDatabaseBackend: Remove use of QSqlDatabase::exec() 2023-03-18 02:20:03 +01:00
Jonas Kvinge
c95295d8b4 Console: Remove use of QSqlDatabase::exec() 2023-03-18 01:49:03 +01:00
Strawbs Bot
658dce2607 Update translations 2023-03-18 01:01:44 +01:00
Jonas Kvinge
01f8d0a27e SavedGroupingManager: Add missing collectionsettingspage include 2023-03-18 01:01:28 +01:00
Jonas Kvinge
39e1bfc84f CollectionBackend: Add default for ResetStatistics save_tags to false 2023-03-18 01:00:28 +01:00
Jonas Kvinge
e394416fa7 CollectionFilterWidget: Add missing collectionsettingspage include 2023-03-18 00:59:39 +01:00
Strawbs Bot
f1108bc0e2 Update translations 2023-03-15 01:01:28 +01:00
Jonas Kvinge
ff31815d49 SmartPlaylistSearchTerm: Revert if / else 2023-03-11 18:16:48 +01:00
Jonas Kvinge
604aa63b47 SmartPlaylists: Rename variable collection to collection_backend 2023-03-11 17:18:35 +01:00
Jonas Kvinge
ec4d036f50 SmartPlaylistsModel: Add missing endArray 2023-03-11 17:17:23 +01:00
Jonas Kvinge
1bf1c4ac63 nsi: Simplify macro code 2023-03-09 16:14:32 +01:00
Jonas Kvinge
981d46fbd4 Link abseil_dll for MSVC 2023-03-09 16:14:32 +01:00
Jonas Kvinge
eb6a353c31 nsi: Add libabsl 2023-03-09 16:14:32 +01:00
Strawbs Bot
ff673b1941 Update translations 2023-03-09 01:01:45 +01:00
Jonas Kvinge
8b55cf8a3a Check for policy 2023-03-08 19:01:18 +01:00
Jonas Kvinge
d8682b4403 Workaround absl linking errors on Windows with protobuf 3.22 2023-03-08 18:57:52 +01:00
Jonas Kvinge
40a4bf195a Link with Protobuf_LIBRARIES 2023-03-08 18:56:15 +01:00
Jonas Kvinge
312c5cbc3f macosutils: Formatting 2023-03-08 01:03:57 +01:00
Strawbs Bot
f314c56ef0 Update translations 2023-03-08 01:02:01 +01:00
Jonas Kvinge
ea8e5180ff Detect if running under Rosetta 2023-03-07 23:04:00 +01:00
Jonas Kvinge
7f76c3f2ce CI: Add Ubuntu Lunar 2023-03-07 20:53:49 +01:00
Jonas Kvinge
e4c5e99d0f MainWindow: Add macOS warning if running on different CPU 2023-03-07 20:14:37 +01:00
Jonas Kvinge
80cfca5de2 CI: Set HOMEBREW_NO_INSTALL_FROM_API for macOS 2023-03-07 01:15:38 +01:00
Strawbs Bot
9556a14de9 Update translations 2023-03-07 01:02:32 +01:00
Jonas Kvinge
2025fc8325 Update list of lyrics providers in README.md and dist files 2023-03-06 21:50:42 +01:00
Jonas Kvinge
2dd0f6a9ba Remove AudD lyrics provider 2023-03-06 21:46:00 +01:00
Jonas Kvinge
a42039d6e5 Lyrics: Match both album artist and artist in lyrics results 2023-03-06 21:42:59 +01:00
Jonas Kvinge
7fafa8adfb QtFSListener: Log watcher errors 2023-03-05 01:23:28 +01:00
Jonas Kvinge
f789657552 SingleApplication: Add boost include dirs 2023-03-05 01:04:57 +01:00
Jonas Kvinge
3e183bc10e SingleApplication: Change lambda capture order 2023-03-05 00:10:34 +01:00
Jonas Kvinge
16e5d93be3 SingleApplication: Add memory include 2023-03-05 00:08:45 +01:00
Jonas Kvinge
6515e06a13 SingleApplication: Refactor startup code 2023-03-04 23:47:36 +01:00
Jonas Kvinge
9ae0b32318 nsi: Update ffmpeg dlls for MinGW 2023-03-04 23:43:40 +01:00
Jonas Kvinge
4cb3bc4185 SingleApplication: Share code between SingleApplication and SingleCoreApplication 2023-03-04 20:48:21 +01:00
Jonas Kvinge
f9ca24598e Turn on git revision 2023-03-04 16:13:45 +01:00
Jonas Kvinge
3ccc892d6a Release 1.0.15 2023-03-04 15:46:51 +01:00
Jonas Kvinge
d7cacea843 Update Changelog 2023-03-04 15:45:33 +01:00
Strawbs Bot
e30233ac74 Update translations 2023-03-03 01:01:55 +01:00
Jonas Kvinge
7c57631fcf SingleApplication: Only include QNativeIpcKey with Qt 6.6 and higher 2023-03-02 23:03:33 +01:00
Jonas Kvinge
0adc084dad SingleApplication: Use new QSharedMemory constructor with Qt 6.6 and higher 2023-03-02 22:57:33 +01:00
Jonas Kvinge
e22199817c SingleApplicationPrivate: Move variable 2023-03-02 22:56:41 +01:00
Jonas Kvinge
78f691d006 SingleApplication: Remove semicolon 2023-03-02 22:56:17 +01:00
Jonas Kvinge
749bae1f16 SingleApplication: Use enum class for Mode 2023-03-02 22:55:50 +01:00
Jonas Kvinge
1043e24322 SingleApplication: Remove exit 2023-03-02 22:55:21 +01:00
Jonas Kvinge
a6d10b1fa7 GstEnginePipeline: Check that audio bin exists before unref 2023-02-27 18:50:09 +01:00
Jonas Kvinge
a3159423f8 nsi: Update libiconv for MSVC 2023-02-24 22:09:35 +01:00
Jonas Kvinge
ecb5ca321b CI: Use Windows 2022 and msvc toolset 14.3 2023-02-24 22:03:30 +01:00
Jonas Kvinge
fd827fdfd8 Add CodeQL 2023-02-22 18:53:21 +01:00
Jonas Kvinge
92d77b14d5 CI: Minor cleanup 2023-02-21 18:43:01 +01:00
dependabot[bot]
be67d89d8b Bump vmactions/freebsd-vm from 0.2.9 to 0.3.0
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 0.2.9 to 0.3.0.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v0.2.9...v0.3.0)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-21 01:59:45 +00:00
Jonas Kvinge
ddd1ce732a dependabot formatting 2023-02-20 20:32:04 +01:00
Jonas Kvinge
012d82183c Add dependabot 2023-02-20 20:28:41 +01:00
Jonas Kvinge
f9c98ebcb3 CI: Use build_type specific container image for mingw 2023-02-20 18:47:28 +01:00
Jonas Kvinge
10fe861dde nsi: Move protobuf to common files 2023-02-19 11:01:28 +01:00
Strawbs Bot
16c027ecab Update translations 2023-02-19 01:21:50 +01:00
Jonas Kvinge
cc578e7cc5 Sonogram: Remove unused parameter variable 2023-02-18 18:44:14 +01:00
Jonas Kvinge
6972d3c4f9 Sonogram: Change int to size_t 2023-02-18 17:52:01 +01:00
Jonas Kvinge
46b164f2fb nsi: Update protobuf dll 2023-02-18 17:30:38 +01:00
Jonas Kvinge
5431307527 CollectionFilterOptions: Match album artist too 2023-02-18 17:05:41 +01:00
Jonas Kvinge
79bf194ed6 CollectionModel: Fix compilation requirement query
Fixes #1140
2023-02-18 16:40:12 +01:00
Jonas Kvinge
506e670aa7 CI: Fix macOS build 2023-02-18 15:21:59 +01:00
Jonas Kvinge
fdfe164dd1 SmartPlaylistsModel: Save smart playlist type as int 2023-02-18 14:34:02 +01:00
Jonas Kvinge
af37056179 Change TrackChangeType to enum 2023-02-18 14:33:43 +01:00
Jonas Kvinge
b0fc7187cf PlaylistSettingsPage: Add Q_DECLARE_METATYPE for PathType 2023-02-18 14:33:19 +01:00
Jonas Kvinge
33ad1a7a86 Song: Add Q_DECLARE_METATYPE for Source and FileType 2023-02-18 14:33:01 +01:00
Jonas Kvinge
dd72fb4ca5 Use C++11 enum class 2023-02-18 14:09:30 +01:00
Jonas Kvinge
e6c5f76872 FancyTabWidget: Remove extra namespace 2023-02-18 14:09:30 +01:00
Jonas Kvinge
14aa22d590 Stands4LyricsProvider: Use direct URL if API usage limit exceeds 2023-02-18 14:09:30 +01:00
Strawbs Bot
5ed4293641 Update translations 2023-02-12 01:02:28 +01:00
Jonas Kvinge
f9fefcda57 Update Changelog 2023-02-11 01:49:10 +01:00
Jonas Kvinge
99b40293db Stands4LyricsProvider: Use percent encoding 2023-02-11 01:39:14 +01:00
Jonas Kvinge
9b06e85f94 Stands4LyricsProvider: Use API for search 2023-02-11 01:33:33 +01:00
Jonas Kvinge
93d1d40ea5 LyricsProvider: Default to empty results 2023-02-11 01:33:33 +01:00
Strawbs Bot
98597c047a Update translations 2023-02-11 01:19:51 +01:00
Jonas Kvinge
a5c1f4b0ee EditTagDialog: Fix saving play statistics
Fixes #1124
2023-02-10 22:51:48 +01:00
Jonas Kvinge
3d4c98d981 Playlist: Fix tag inline editing for steams
Fixes #1130
2023-02-10 22:49:57 +01:00
Jonas Kvinge
384e7dedb5 PlaylistView: Move ifdef 2023-02-10 22:47:55 +01:00
Jonas Kvinge
7df4453560 OrganizeFormat: Use suffix instead of complete suffix
Fixes #1136
2023-02-10 22:47:11 +01:00
Jonas Kvinge
d406a1c341 GstEnginePipeline: Use playbin3 with GStreamer 1.22.0 and higher 2023-02-10 22:43:55 +01:00
Jonas Kvinge
6671d97b4a GstEnginePipeline: Free audio bin in destructor
When the audio bin failed to initialize, we tried to disconnect signals and probes after the audio bin was already freed.
Instead, free the audio bin in the destructor after disconnecting signals and probes.

Fixes #1133 and #1123
2023-02-10 22:42:37 +01:00
Strawbs Bot
d02de72830 Update translations 2023-02-06 01:01:22 +01:00
Strawbs Bot
08f5172028 Update translations 2023-01-23 01:01:39 +01:00
4fury-c3440d8
04f062547d TagReaderTagParser: Fix timeconstants.h include 2023-01-22 13:22:02 +01:00
Jonas Kvinge
4717d783dc Stands4LyricsProvider: Finish search when no lyrics are found 2023-01-21 15:55:47 +01:00
Strawbs Bot
93af064b36 Update translations 2023-01-21 01:02:43 +01:00
Jonas Kvinge
c78d73d727 LastPlayedItemDelegate: Show blank for zero or invalid time 2023-01-20 23:45:33 +01:00
Jonas Kvinge
b69b3228be DateItemDelegate: Show blank for zero or invalid time 2023-01-20 23:45:19 +01:00
Jonas Kvinge
377f54700d TidalRequest: Make cover optional 2023-01-20 23:35:47 +01:00
Jonas Kvinge
d276339c80 Add lyrics from stands4 (lyrics.com) 2023-01-20 22:48:52 +01:00
Jonas Kvinge
b982a6a762 LyricsProvider: Improve parsing from HTML function 2023-01-20 22:45:05 +01:00
Jonas Kvinge
536fe637aa About: Remove e-mail addresses 2023-01-20 19:32:49 +01:00
Strawbs Bot
69f36eaa25 Update translations 2023-01-15 01:01:48 +01:00
Strawbs Bot
d6927a70bb Update translations 2023-01-14 01:01:51 +01:00
Jonas Kvinge
1b1892a187 Sonogram: Fix parameter mismatch in header 2023-01-13 23:46:38 +01:00
Sungrak Choi
5bea71cd5c Add Sonogram analyzer
Co-Authored-By: Jonas Kvinge <jonas@jkvinge.net>
2023-01-13 23:41:43 +01:00
Jonas Kvinge
bb8d4e70ae Turn on git revision 2023-01-13 22:53:39 +01:00
Jonas Kvinge
8d8c7e8b7b Release 1.0.14 2023-01-13 22:21:54 +01:00
Jonas Kvinge
9ff1f4d7b4 Update Changelog 2023-01-13 22:15:07 +01:00
Strawbs Bot
3a60dfe025 Update translations 2023-01-13 01:01:53 +01:00
Jonas Kvinge
d358854e16 Transcoder: Use static cast 2023-01-12 23:37:12 +01:00
Jonas Kvinge
129587e94a TranscoderOptionsMP3: Change default settings 2023-01-12 23:36:34 +01:00
Jonas Kvinge
0f575f4639 main: Remove explicitly enabling debug messages
Fixes #1106
2023-01-11 22:08:58 +01:00
Jonas Kvinge
7aac741571 GstEnginePipeline: Fix setting initial volume
Fixes #1104
2023-01-11 18:52:14 +01:00
Jonas Kvinge
b8a9da8a4e GstEngine: Use QUrl::isLocalFile 2023-01-10 18:26:42 +01:00
Jonas Kvinge
be6b974334 MoodbarPipeline: Fix saving moodbar if the URL contains host
Fixes #1101
2023-01-10 18:18:49 +01:00
knuxify
194e81205b CollectionBackendTest: Fix compile 2023-01-10 17:38:30 +01:00
Jonas Kvinge
d711dcc99d Turn on git revision 2023-01-10 00:46:49 +01:00
559 changed files with 27810 additions and 24833 deletions

View File

@@ -86,7 +86,7 @@ ContinuationIndentWidth: 2
Cpp11BracedListStyle: false
DeriveLineEnding: true
DerivePointerAlignment: false
DisableFormat: false
DisableFormat: true
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false

6
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: daily

File diff suppressed because it is too large Load Diff

10
3rdparty/README.md vendored
View File

@@ -1,15 +1,13 @@
3rdparty libraries located in this directory
============================================
singleapplication
KDSingleApplication
-----------------
This is a small static library used by Strawberry to prevent it from starting twice per user session.
If the user tries to start strawberry twice, the main window will maximize instead of starting another instance.
If you dynamically link to your systems version, you'll need two versions, one defined as QApplication and
one as a QCoreApplication.
It is included here because it is not packed by distros and is also used on macOS and Windows.
It is also used to pass command-line options through to the first instance.
URL: https://github.com/itay-grudev/SingleApplication
URL: https://github.com/KDAB/KDSingleApplication/
SPMediaKeyTap
@@ -27,4 +25,4 @@ Can safely be deleted on other platforms.
getopt
------
getopt included only when compiling with MSVC on Windows.
getopt included only when compiling on Windows.

View File

@@ -1,2 +1,3 @@
add_library(getopt STATIC getopt.c)
add_library(getopt STATIC getopt.cpp)
target_compile_definitions(getopt PRIVATE -DSTATIC_GETOPT -D_UNICODE)
target_include_directories(getopt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -1,562 +0,0 @@
/* $OpenBSD: getopt_long.c,v 1.23 2007/10/31 12:34:57 chl Exp $ */
/* $NetBSD: getopt_long.c,v 1.15 2002/01/31 22:43:40 tv Exp $ */
/*
* Copyright (c) 2002 Todd C. Miller <Todd.Miller@courtesan.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* Sponsored in part by the Defense Advanced Research Projects
* Agency (DARPA) and Air Force Research Laboratory, Air Force
* Materiel Command, USAF, under agreement number F39502-99-1-0512.
*/
/*-
* Copyright (c) 2000 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Dieter Baron and Thomas Klausner.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <stdarg.h>
#include <stdio.h>
#include <windows.h>
#define REPLACE_GETOPT /* use this getopt as the system getopt(3) */
#ifdef REPLACE_GETOPT
int opterr = 1; /* if error message should be printed */
int optind = 1; /* index into parent argv vector */
int optopt = '?'; /* character checked for validity */
#undef optreset /* see getopt.h */
#define optreset __mingw_optreset
int optreset; /* reset getopt */
char *optarg; /* argument associated with option */
#endif
#define PRINT_ERROR ((opterr) && (*options != ':'))
#define FLAG_PERMUTE 0x01 /* permute non-options to the end of argv */
#define FLAG_ALLARGS 0x02 /* treat non-options as args to option "-1" */
#define FLAG_LONGONLY 0x04 /* operate as getopt_long_only */
/* return values */
#define BADCH (int)'?'
#define BADARG ((*options == ':') ? (int)':' : (int)'?')
#define INORDER (int)1
#ifndef __CYGWIN__
#define __progname __argv[0]
#else
extern char __declspec(dllimport) *__progname;
#endif
#ifdef __CYGWIN__
static char EMSG[] = "";
#else
#define EMSG ""
#endif
static int getopt_internal(int, char * const *, const char *,
const struct option *, int *, int);
static int parse_long_options(char * const *, const char *,
const struct option *, int *, int);
static int gcd(int, int);
static void permute_args(int, int, int, char * const *);
static char *place = EMSG; /* option letter processing */
/* XXX: set optreset to 1 rather than these two */
static int nonopt_start = -1; /* first non option argument (for permute) */
static int nonopt_end = -1; /* first option after non options (for permute) */
/* Error messages */
static const char recargchar[] = "option requires an argument -- %c";
static const char recargstring[] = "option requires an argument -- %s";
static const char ambig[] = "ambiguous option -- %.*s";
static const char noarg[] = "option doesn't take an argument -- %.*s";
static const char illoptchar[] = "unknown option -- %c";
static const char illoptstring[] = "unknown option -- %s";
static void
_vwarnx(const char *fmt,va_list ap)
{
(void)fprintf(stderr,"%s: ",__progname);
if (fmt != NULL)
(void)vfprintf(stderr,fmt,ap);
(void)fprintf(stderr,"\n");
}
static void
warnx(const char *fmt,...)
{
va_list ap;
va_start(ap,fmt);
_vwarnx(fmt,ap);
va_end(ap);
}
/*
* Compute the greatest common divisor of a and b.
*/
static int
gcd(int a, int b)
{
int c;
c = a % b;
while (c != 0) {
a = b;
b = c;
c = a % b;
}
return (b);
}
/*
* Exchange the block from nonopt_start to nonopt_end with the block
* from nonopt_end to opt_end (keeping the same order of arguments
* in each block).
*/
static void
permute_args(int panonopt_start, int panonopt_end, int opt_end,
char * const *nargv)
{
int cstart, cyclelen, i, j, ncycle, nnonopts, nopts, pos;
char *swap;
/*
* compute lengths of blocks and number and size of cycles
*/
nnonopts = panonopt_end - panonopt_start;
nopts = opt_end - panonopt_end;
ncycle = gcd(nnonopts, nopts);
cyclelen = (opt_end - panonopt_start) / ncycle;
for (i = 0; i < ncycle; i++) {
cstart = panonopt_end+i;
pos = cstart;
for (j = 0; j < cyclelen; j++) {
if (pos >= panonopt_end)
pos -= nnonopts;
else
pos += nopts;
swap = nargv[pos];
/* LINTED const cast */
((char **) nargv)[pos] = nargv[cstart];
/* LINTED const cast */
((char **)nargv)[cstart] = swap;
}
}
}
/*
* parse_long_options --
* Parse long options in argc/argv argument vector.
* Returns -1 if short_too is set and the option does not match long_options.
*/
static int
parse_long_options(char * const *nargv, const char *options,
const struct option *long_options, int *idx, int short_too)
{
char *current_argv, *has_equal;
size_t current_argv_len;
int i, ambiguous, match;
#define IDENTICAL_INTERPRETATION(_x, _y) \
(long_options[(_x)].has_arg == long_options[(_y)].has_arg && \
long_options[(_x)].flag == long_options[(_y)].flag && \
long_options[(_x)].val == long_options[(_y)].val)
current_argv = place;
match = -1;
ambiguous = 0;
optind++;
if ((has_equal = strchr(current_argv, '=')) != NULL) {
/* argument found (--option=arg) */
current_argv_len = has_equal - current_argv;
has_equal++;
} else
current_argv_len = strlen(current_argv);
for (i = 0; long_options[i].name; i++) {
/* find matching long option */
if (strncmp(current_argv, long_options[i].name,
current_argv_len))
continue;
if (strlen(long_options[i].name) == current_argv_len) {
/* exact match */
match = i;
ambiguous = 0;
break;
}
/*
* If this is a known short option, don't allow
* a partial match of a single character.
*/
if (short_too && current_argv_len == 1)
continue;
if (match == -1) /* partial match */
match = i;
else if (!IDENTICAL_INTERPRETATION(i, match))
ambiguous = 1;
}
if (ambiguous) {
/* ambiguous abbreviation */
if (PRINT_ERROR)
warnx(ambig, (int)current_argv_len,
current_argv);
optopt = 0;
return (BADCH);
}
if (match != -1) { /* option found */
if (long_options[match].has_arg == no_argument
&& has_equal) {
if (PRINT_ERROR)
warnx(noarg, (int)current_argv_len,
current_argv);
/*
* XXX: GNU sets optopt to val regardless of flag
*/
if (long_options[match].flag == NULL)
optopt = long_options[match].val;
else
optopt = 0;
return (BADARG);
}
if (long_options[match].has_arg == required_argument ||
long_options[match].has_arg == optional_argument) {
if (has_equal)
optarg = has_equal;
else if (long_options[match].has_arg ==
required_argument) {
/*
* optional argument doesn't use next nargv
*/
optarg = nargv[optind++];
}
}
if ((long_options[match].has_arg == required_argument)
&& (optarg == NULL)) {
/*
* Missing argument; leading ':' indicates no error
* should be generated.
*/
if (PRINT_ERROR)
warnx(recargstring,
current_argv);
/*
* XXX: GNU sets optopt to val regardless of flag
*/
if (long_options[match].flag == NULL)
optopt = long_options[match].val;
else
optopt = 0;
--optind;
return (BADARG);
}
} else { /* unknown option */
if (short_too) {
--optind;
return (-1);
}
if (PRINT_ERROR)
warnx(illoptstring, current_argv);
optopt = 0;
return (BADCH);
}
if (idx)
*idx = match;
if (long_options[match].flag) {
*long_options[match].flag = long_options[match].val;
return (0);
} else
return (long_options[match].val);
#undef IDENTICAL_INTERPRETATION
}
/*
* getopt_internal --
* Parse argc/argv argument vector. Called by user level routines.
*/
static int
getopt_internal(int nargc, char * const *nargv, const char *options,
const struct option *long_options, int *idx, int flags)
{
char *oli; /* option letter list index */
int optchar, short_too;
static int posixly_correct = -1;
if (options == NULL)
return (-1);
/*
* XXX Some GNU programs (like cvs) set optind to 0 instead of
* XXX using optreset. Work around this braindamage.
*/
if (optind == 0)
optind = optreset = 1;
/*
* Disable GNU extensions if POSIXLY_CORRECT is set or options
* string begins with a '+'.
*
* CV, 2009-12-14: Check POSIXLY_CORRECT anew if optind == 0 or
* optreset != 0 for GNU compatibility.
*/
if (posixly_correct == -1 || optreset != 0)
posixly_correct = (GetEnvironmentVariableW(L"POSIXLY_CORRECT", NULL, 0) != 0);
if (*options == '-')
flags |= FLAG_ALLARGS;
else if (posixly_correct || *options == '+')
flags &= ~FLAG_PERMUTE;
if (*options == '+' || *options == '-')
options++;
optarg = NULL;
if (optreset)
nonopt_start = nonopt_end = -1;
start:
if (optreset || !*place) { /* update scanning pointer */
optreset = 0;
if (optind >= nargc) { /* end of argument vector */
place = EMSG;
if (nonopt_end != -1) {
/* do permutation, if we have to */
permute_args(nonopt_start, nonopt_end,
optind, nargv);
optind -= nonopt_end - nonopt_start;
}
else if (nonopt_start != -1) {
/*
* If we skipped non-options, set optind
* to the first of them.
*/
optind = nonopt_start;
}
nonopt_start = nonopt_end = -1;
return (-1);
}
if (*(place = nargv[optind]) != '-' ||
(place[1] == '\0' && strchr(options, '-') == NULL)) {
place = EMSG; /* found non-option */
if (flags & FLAG_ALLARGS) {
/*
* GNU extension:
* return non-option as argument to option 1
*/
optarg = nargv[optind++];
return (INORDER);
}
if (!(flags & FLAG_PERMUTE)) {
/*
* If no permutation wanted, stop parsing
* at first non-option.
*/
return (-1);
}
/* do permutation */
if (nonopt_start == -1)
nonopt_start = optind;
else if (nonopt_end != -1) {
permute_args(nonopt_start, nonopt_end,
optind, nargv);
nonopt_start = optind -
(nonopt_end - nonopt_start);
nonopt_end = -1;
}
optind++;
/* process next argument */
goto start;
}
if (nonopt_start != -1 && nonopt_end == -1)
nonopt_end = optind;
/*
* If we have "-" do nothing, if "--" we are done.
*/
if (place[1] != '\0' && *++place == '-' && place[1] == '\0') {
optind++;
place = EMSG;
/*
* We found an option (--), so if we skipped
* non-options, we have to permute.
*/
if (nonopt_end != -1) {
permute_args(nonopt_start, nonopt_end,
optind, nargv);
optind -= nonopt_end - nonopt_start;
}
nonopt_start = nonopt_end = -1;
return (-1);
}
}
/*
* Check long options if:
* 1) we were passed some
* 2) the arg is not just "-"
* 3) either the arg starts with -- we are getopt_long_only()
*/
if (long_options != NULL && place != nargv[optind] &&
(*place == '-' || (flags & FLAG_LONGONLY))) {
short_too = 0;
if (*place == '-')
place++; /* --foo long option */
else if (*place != ':' && strchr(options, *place) != NULL)
short_too = 1; /* could be short option too */
optchar = parse_long_options(nargv, options, long_options,
idx, short_too);
if (optchar != -1) {
place = EMSG;
return (optchar);
}
}
if ((optchar = (int)*place++) == (int)':' ||
(optchar == (int)'-' && *place != '\0') ||
(oli = strchr(options, optchar)) == NULL) {
/*
* If the user specified "-" and '-' isn't listed in
* options, return -1 (non-option) as per POSIX.
* Otherwise, it is an unknown option character (or ':').
*/
if (optchar == (int)'-' && *place == '\0')
return (-1);
if (!*place)
++optind;
if (PRINT_ERROR)
warnx(illoptchar, optchar);
optopt = optchar;
return (BADCH);
}
if (long_options != NULL && optchar == 'W' && oli[1] == ';') {
/* -W long-option */
if (*place) /* no space */
/* NOTHING */;
else if (++optind >= nargc) { /* no arg */
place = EMSG;
if (PRINT_ERROR)
warnx(recargchar, optchar);
optopt = optchar;
return (BADARG);
} else /* white space */
place = nargv[optind];
optchar = parse_long_options(nargv, options, long_options,
idx, 0);
place = EMSG;
return (optchar);
}
if (*++oli != ':') { /* doesn't take argument */
if (!*place)
++optind;
} else { /* takes (optional) argument */
optarg = NULL;
if (*place) /* no white space */
optarg = place;
else if (oli[1] != ':') { /* arg not optional */
if (++optind >= nargc) { /* no arg */
place = EMSG;
if (PRINT_ERROR)
warnx(recargchar, optchar);
optopt = optchar;
return (BADARG);
} else
optarg = nargv[optind];
}
place = EMSG;
++optind;
}
/* dump back option letter */
return (optchar);
}
#ifdef REPLACE_GETOPT
/*
* getopt --
* Parse argc/argv argument vector.
*
* [eventually this will replace the BSD getopt]
*/
int
getopt(int nargc, char * const *nargv, const char *options)
{
/*
* We don't pass FLAG_PERMUTE to getopt_internal() since
* the BSD getopt(3) (unlike GNU) has never done this.
*
* Furthermore, since many privileged programs call getopt()
* before dropping privileges it makes sense to keep things
* as simple (and bug-free) as possible.
*/
return (getopt_internal(nargc, nargv, options, NULL, NULL, 0));
}
#endif /* REPLACE_GETOPT */
/*
* getopt_long --
* Parse argc/argv argument vector.
*/
int
getopt_long(int nargc, char * const *nargv, const char *options,
const struct option *long_options, int *idx)
{
return (getopt_internal(nargc, nargv, options, long_options, idx,
FLAG_PERMUTE));
}
/*
* getopt_long_only --
* Parse argc/argv argument vector.
*/
int
getopt_long_only(int nargc, char * const *nargv, const char *options,
const struct option *long_options, int *idx)
{
return (getopt_internal(nargc, nargv, options, long_options, idx,
FLAG_PERMUTE|FLAG_LONGONLY));
}

753
3rdparty/getopt/getopt.cpp vendored Normal file
View File

@@ -0,0 +1,753 @@
/* Getopt for Microsoft C
This code is a modification of the Free Software Foundation, Inc.
Getopt library for parsing command line argument the purpose was
to provide a Microsoft Visual C friendly derivative. This code
provides functionality for both Unicode and Multibyte builds.
Date: 02/03/2011 - Ludvik Jerabek - Initial Release
Version: 1.1
Comment: Supports getopt, getopt_long, and getopt_long_only
and POSIXLY_CORRECT environment flag
License: LGPL
Revisions:
02/03/2011 - Ludvik Jerabek - Initial Release
02/20/2011 - Ludvik Jerabek - Fixed compiler warnings at Level 4
07/05/2011 - Ludvik Jerabek - Added no_argument, required_argument, optional_argument defs
08/03/2011 - Ludvik Jerabek - Fixed non-argument runtime bug which caused runtime exception
08/09/2011 - Ludvik Jerabek - Added code to export functions for DLL and LIB
02/15/2012 - Ludvik Jerabek - Fixed _GETOPT_THROW definition missing in implementation file
08/01/2012 - Ludvik Jerabek - Created separate functions for char and wchar_t characters so single dll can do both unicode and ansi
10/15/2012 - Ludvik Jerabek - Modified to match latest GNU features
06/19/2015 - Ludvik Jerabek - Fixed maximum option limitation caused by option_a (255) and option_w (65535) structure val variable
09/24/2022 - Ludvik Jerabek - Updated to match most recent getopt release
09/25/2022 - Ludvik Jerabek - Fixed memory allocation (malloc call) issue for wchar_t*
**DISCLAIMER**
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING, BUT Not LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, OR NON-INFRINGEMENT. SOME JURISDICTIONS DO NOT ALLOW THE
EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT
APPLY TO YOU. IN NO EVENT WILL I BE LIABLE TO ANY PARTY FOR ANY
DIRECT, INDIRECT, SPECIAL OR OTHER CONSEQUENTIAL DAMAGES FOR ANY
USE OF THIS MATERIAL INCLUDING, WITHOUT LIMITATION, ANY LOST
PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR OTHER DATA ON
YOUR INFORMATION HANDLING SYSTEM OR OTHERWISE, EVEN If WE ARE
EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
*/
#define _CRT_SECURE_NO_WARNINGS
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <malloc.h>
#include "getopt.h"
#ifdef __cplusplus
# define _GETOPT_THROW throw()
#else
# define _GETOPT_THROW
#endif
int optind = 1;
int opterr = 1;
int optopt = '?';
enum ENUM_ORDERING {
REQUIRE_ORDER,
PERMUTE,
RETURN_IN_ORDER
};
//
//
// Ansi structures and functions follow
//
//
static struct _getopt_data_a {
int optind;
int opterr;
int optopt;
char *optarg;
int __initialized;
char *__nextchar;
enum ENUM_ORDERING __ordering;
int __first_nonopt;
int __last_nonopt;
} getopt_data_a;
char *optarg_a;
static void exchange_a(char **argv, struct _getopt_data_a *d) {
int bottom = d->__first_nonopt;
int middle = d->__last_nonopt;
int top = d->optind;
char *tem;
while (top > middle && middle > bottom) {
if (top - middle > middle - bottom) {
int len = middle - bottom;
int i;
for (i = 0; i < len; i++) {
tem = argv[bottom + i];
argv[bottom + i] = argv[top - (middle - bottom) + i];
argv[top - (middle - bottom) + i] = tem;
}
top -= len;
}
else {
int len = top - middle;
int i;
for (i = 0; i < len; i++) {
tem = argv[bottom + i];
argv[bottom + i] = argv[middle + i];
argv[middle + i] = tem;
}
bottom += len;
}
}
d->__first_nonopt += (d->optind - d->__last_nonopt);
d->__last_nonopt = d->optind;
}
static int process_long_option_a(int argc, char **argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int print_errors, const char *prefix);
static int process_long_option_a(int argc, char **argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int print_errors, const char *prefix) {
assert(longopts != NULL);
char *nameend;
size_t namelen;
const struct option_a *p;
const struct option_a *pfound = NULL;
int n_options;
int option_index = 0;
for (nameend = d->__nextchar; *nameend && *nameend != '='; nameend++)
;
namelen = nameend - d->__nextchar;
for (p = longopts, n_options = 0; p->name; p++, n_options++)
if (!strncmp(p->name, d->__nextchar, namelen) && namelen == strlen(p->name)) {
pfound = p;
option_index = n_options;
break;
}
if (pfound == NULL) {
unsigned char *ambig_set = NULL;
int ambig_fallback = 0;
int indfound = -1;
for (p = longopts, option_index = 0; p->name; p++, option_index++)
if (!strncmp(p->name, d->__nextchar, namelen)) {
if (pfound == NULL) {
pfound = p;
indfound = option_index;
}
else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val) {
if (!ambig_fallback) {
if (!print_errors)
ambig_fallback = 1;
else if (!ambig_set) {
if ((ambig_set = reinterpret_cast<unsigned char *>(malloc(n_options * sizeof(char)))) == NULL)
ambig_fallback = 1;
if (ambig_set) {
memset(ambig_set, 0, n_options * sizeof(char));
ambig_set[indfound] = 1;
}
}
if (ambig_set)
ambig_set[option_index] = 1;
}
}
}
if (ambig_set || ambig_fallback) {
if (print_errors) {
if (ambig_fallback)
fprintf(stderr, "%s: option '%s%s' is ambiguous\n", argv[0], prefix, d->__nextchar);
else {
_lock_file(stderr);
fprintf(stderr, "%s: option '%s%s' is ambiguous; possibilities:", argv[0], prefix, d->__nextchar);
for (option_index = 0; option_index < n_options; option_index++)
if (ambig_set[option_index])
fprintf(stderr, " '%s%s'", prefix, longopts[option_index].name);
fprintf(stderr, "\n");
_unlock_file(stderr);
}
}
free(ambig_set);
d->__nextchar += strlen(d->__nextchar);
d->optind++;
d->optopt = 0;
return '?';
}
option_index = indfound;
}
if (pfound == NULL) {
if (!long_only || argv[d->optind][1] == '-' || strchr(optstring, *d->__nextchar) == NULL) {
if (print_errors)
fprintf(stderr, "%s: unrecognized option '%s%s'\n", argv[0], prefix, d->__nextchar);
d->__nextchar = NULL;
d->optind++;
d->optopt = 0;
return '?';
}
return -1;
}
d->optind++;
d->__nextchar = NULL;
if (*nameend) {
if (pfound->has_arg)
d->optarg = nameend + 1;
else {
if (print_errors)
fprintf(stderr, "%s: option '%s%s' doesn't allow an argument\n", argv[0], prefix, pfound->name);
d->optopt = pfound->val;
return '?';
}
}
else if (pfound->has_arg == 1) {
if (d->optind < argc)
d->optarg = argv[d->optind++];
else {
if (print_errors)
fprintf(stderr, "%s: option '%s%s' requires an argument\n", argv[0], prefix, pfound->name);
d->optopt = pfound->val;
return optstring[0] == ':' ? ':' : '?';
}
}
if (longind != NULL)
*longind = option_index;
if (pfound->flag) {
*(pfound->flag) = pfound->val;
return 0;
}
return pfound->val;
}
static const char *_getopt_initialize_a(const char *optstring, struct _getopt_data_a *d, int posixly_correct) {
if (d->optind == 0)
d->optind = 1;
d->__first_nonopt = d->__last_nonopt = d->optind;
d->__nextchar = NULL;
if (optstring[0] == '-') {
d->__ordering = RETURN_IN_ORDER;
++optstring;
}
else if (optstring[0] == '+') {
d->__ordering = REQUIRE_ORDER;
++optstring;
}
else if (posixly_correct | !!getenv("POSIXLY_CORRECT"))
d->__ordering = REQUIRE_ORDER;
else
d->__ordering = PERMUTE;
d->__initialized = 1;
return optstring;
}
int _getopt_internal_r_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int posixly_correct);
int _getopt_internal_r_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int posixly_correct) {
int print_errors = d->opterr;
if (argc < 1)
return -1;
d->optarg = NULL;
if (d->optind == 0 || !d->__initialized)
optstring = _getopt_initialize_a(optstring, d, posixly_correct);
else if (optstring[0] == '-' || optstring[0] == '+')
optstring++;
if (optstring[0] == ':')
print_errors = 0;
if (d->__nextchar == NULL || *d->__nextchar == '\0') {
if (d->__last_nonopt > d->optind)
d->__last_nonopt = d->optind;
if (d->__first_nonopt > d->optind)
d->__first_nonopt = d->optind;
if (d->__ordering == PERMUTE) {
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
exchange_a(const_cast<char **>(argv), d);
else if (d->__last_nonopt != d->optind)
d->__first_nonopt = d->optind;
while (d->optind < argc && (argv[d->optind][0] != '-' || argv[d->optind][1] == '\0'))
d->optind++;
d->__last_nonopt = d->optind;
}
if (d->optind != argc && !strcmp(argv[d->optind], "--")) {
d->optind++;
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
exchange_a(const_cast<char **>(argv), d);
else if (d->__first_nonopt == d->__last_nonopt)
d->__first_nonopt = d->optind;
d->__last_nonopt = argc;
d->optind = argc;
}
if (d->optind == argc) {
if (d->__first_nonopt != d->__last_nonopt)
d->optind = d->__first_nonopt;
return -1;
}
if (argv[d->optind][0] != '-' || argv[d->optind][1] == '\0') {
if (d->__ordering == REQUIRE_ORDER)
return -1;
d->optarg = argv[d->optind++];
return 1;
}
if (longopts) {
if (argv[d->optind][1] == '-') {
d->__nextchar = argv[d->optind] + 2;
return process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts, longind, long_only, d, print_errors, "--");
}
if (long_only && (argv[d->optind][2] || !strchr(optstring, argv[d->optind][1]))) {
int code;
d->__nextchar = argv[d->optind] + 1;
code = process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts,
longind, long_only, d,
print_errors, "-");
if (code != -1)
return code;
}
}
d->__nextchar = argv[d->optind] + 1;
}
{
char c = *d->__nextchar++;
const char *temp = strchr(optstring, c);
if (*d->__nextchar == '\0')
++d->optind;
if (temp == NULL || c == ':' || c == ';') {
if (print_errors)
fprintf(stderr, "%s: invalid option -- '%c'\n", argv[0], c);
d->optopt = c;
return '?';
}
if (temp[0] == 'W' && temp[1] == ';' && longopts != NULL) {
if (*d->__nextchar != '\0')
d->optarg = d->__nextchar;
else if (d->optind == argc) {
if (print_errors)
fprintf(stderr, "%s: option requires an argument -- '%c'\n", argv[0], c);
d->optopt = c;
if (optstring[0] == ':')
c = ':';
else
c = '?';
return c;
}
else
d->optarg = argv[d->optind];
d->__nextchar = d->optarg;
d->optarg = NULL;
return process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts, longind, 0, d, print_errors, "-W ");
}
if (temp[1] == ':') {
if (temp[2] == ':') {
if (*d->__nextchar != '\0') {
d->optarg = d->__nextchar;
d->optind++;
}
else
d->optarg = NULL;
d->__nextchar = NULL;
}
else {
if (*d->__nextchar != '\0') {
d->optarg = d->__nextchar;
d->optind++;
}
else if (d->optind == argc) {
if (print_errors)
fprintf(stderr, "%s: option requires an argument -- '%c'\n", argv[0], c);
d->optopt = c;
if (optstring[0] == ':')
c = ':';
else
c = '?';
}
else
d->optarg = argv[d->optind++];
d->__nextchar = NULL;
}
}
return c;
}
}
int _getopt_internal_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, int posixly_correct);
int _getopt_internal_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, int posixly_correct) {
int result;
getopt_data_a.optind = optind;
getopt_data_a.opterr = opterr;
result = _getopt_internal_r_a(argc, argv, optstring, longopts, longind, long_only, &getopt_data_a, posixly_correct);
optind = getopt_data_a.optind;
optarg_a = getopt_data_a.optarg;
optopt = getopt_data_a.optopt;
return result;
}
int getopt_a(int argc, char *const *argv, const char *optstring) _GETOPT_THROW {
return _getopt_internal_a(argc, argv, optstring, static_cast<const struct option_a *>(0), static_cast<int *>(0), 0, 0);
}
int getopt_long_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW {
return _getopt_internal_a(argc, argv, options, long_options, opt_index, 0, 0);
}
int getopt_long_only_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW {
return _getopt_internal_a(argc, argv, options, long_options, opt_index, 1, 0);
}
int _getopt_long_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d);
int _getopt_long_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d) {
return _getopt_internal_r_a(argc, argv, options, long_options, opt_index, 0, d, 0);
}
int _getopt_long_only_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d);
int _getopt_long_only_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d) {
return _getopt_internal_r_a(argc, argv, options, long_options, opt_index, 1, d, 0);
}
//
//
// Unicode Structures and Functions
//
//
static struct _getopt_data_w {
int optind;
int opterr;
int optopt;
wchar_t *optarg;
int __initialized;
wchar_t *__nextchar;
enum ENUM_ORDERING __ordering;
int __first_nonopt;
int __last_nonopt;
} getopt_data_w;
wchar_t *optarg_w;
static void exchange_w(wchar_t **argv, struct _getopt_data_w *d) {
int bottom = d->__first_nonopt;
int middle = d->__last_nonopt;
int top = d->optind;
wchar_t *tem;
while (top > middle && middle > bottom) {
if (top - middle > middle - bottom) {
int len = middle - bottom;
int i;
for (i = 0; i < len; i++) {
tem = argv[bottom + i];
argv[bottom + i] = argv[top - (middle - bottom) + i];
argv[top - (middle - bottom) + i] = tem;
}
top -= len;
}
else {
int len = top - middle;
int i;
for (i = 0; i < len; i++) {
tem = argv[bottom + i];
argv[bottom + i] = argv[middle + i];
argv[middle + i] = tem;
}
bottom += len;
}
}
d->__first_nonopt += (d->optind - d->__last_nonopt);
d->__last_nonopt = d->optind;
}
static int process_long_option_w(int argc, wchar_t **argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int print_errors, const wchar_t *prefix) {
assert(longopts != NULL);
wchar_t *nameend;
size_t namelen;
const struct option_w *p;
const struct option_w *pfound = NULL;
int n_options;
int option_index = 0;
for (nameend = d->__nextchar; *nameend && *nameend != L'='; nameend++)
;
namelen = nameend - d->__nextchar;
for (p = longopts, n_options = 0; p->name; p++, n_options++)
if (!wcsncmp(p->name, d->__nextchar, namelen) && namelen == wcslen(p->name)) {
pfound = p;
option_index = n_options;
break;
}
if (pfound == NULL) {
wchar_t *ambig_set = NULL;
int ambig_fallback = 0;
int indfound = -1;
for (p = longopts, option_index = 0; p->name; p++, option_index++)
if (!wcsncmp(p->name, d->__nextchar, namelen)) {
if (pfound == NULL) {
pfound = p;
indfound = option_index;
}
else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val) {
if (!ambig_fallback) {
if (!print_errors)
ambig_fallback = 1;
else if (!ambig_set) {
if ((ambig_set = reinterpret_cast<wchar_t *>(malloc(n_options * sizeof(wchar_t)))) == NULL)
ambig_fallback = 1;
if (ambig_set) {
memset(ambig_set, 0, n_options * sizeof(wchar_t));
ambig_set[indfound] = 1;
}
}
if (ambig_set)
ambig_set[option_index] = 1;
}
}
}
if (ambig_set || ambig_fallback) {
if (print_errors) {
if (ambig_fallback)
fwprintf(stderr, L"%s: option '%s%s' is ambiguous\n", argv[0], prefix, d->__nextchar);
else {
_lock_file(stderr);
fwprintf(stderr, L"%s: option '%s%s' is ambiguous; possibilities:", argv[0], prefix, d->__nextchar);
for (option_index = 0; option_index < n_options; option_index++)
if (ambig_set[option_index])
fwprintf(stderr, L" '%s%s'", prefix, longopts[option_index].name);
fwprintf(stderr, L"\n");
_unlock_file(stderr);
}
}
free(ambig_set);
d->__nextchar += wcslen(d->__nextchar);
d->optind++;
d->optopt = 0;
return L'?';
}
option_index = indfound;
}
if (pfound == NULL) {
if (!long_only || argv[d->optind][1] == L'-' || wcschr(optstring, *d->__nextchar) == NULL) {
if (print_errors)
fwprintf(stderr, L"%s: unrecognized option '%s%s'\n", argv[0], prefix, d->__nextchar);
d->__nextchar = NULL;
d->optind++;
d->optopt = 0;
return L'?';
}
return -1;
}
d->optind++;
d->__nextchar = NULL;
if (*nameend) {
if (pfound->has_arg)
d->optarg = nameend + 1;
else {
if (print_errors)
fwprintf(stderr, L"%s: option '%s%s' doesn't allow an argument\n", argv[0], prefix, pfound->name);
d->optopt = pfound->val;
return L'?';
}
}
else if (pfound->has_arg == 1) {
if (d->optind < argc)
d->optarg = argv[d->optind++];
else {
if (print_errors)
fwprintf(stderr, L"%s: option '%s%s' requires an argument\n", argv[0], prefix, pfound->name);
d->optopt = pfound->val;
return optstring[0] == L':' ? L':' : L'?';
}
}
if (longind != NULL)
*longind = option_index;
if (pfound->flag) {
*(pfound->flag) = pfound->val;
return 0;
}
return pfound->val;
}
static const wchar_t *_getopt_initialize_w(const wchar_t *optstring, struct _getopt_data_w *d, int posixly_correct) {
if (d->optind == 0)
d->optind = 1;
d->__first_nonopt = d->__last_nonopt = d->optind;
d->__nextchar = NULL;
if (optstring[0] == L'-') {
d->__ordering = RETURN_IN_ORDER;
++optstring;
}
else if (optstring[0] == L'+') {
d->__ordering = REQUIRE_ORDER;
++optstring;
}
else if (posixly_correct | !!_wgetenv(L"POSIXLY_CORRECT"))
d->__ordering = REQUIRE_ORDER;
else
d->__ordering = PERMUTE;
d->__initialized = 1;
return optstring;
}
int _getopt_internal_r_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int posixly_correct);
int _getopt_internal_r_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int posixly_correct) {
int print_errors = d->opterr;
if (argc < 1)
return -1;
d->optarg = NULL;
if (d->optind == 0 || !d->__initialized)
optstring = _getopt_initialize_w(optstring, d, posixly_correct);
else if (optstring[0] == L'-' || optstring[0] == L'+')
optstring++;
if (optstring[0] == L':')
print_errors = 0;
#define NONOPTION_P (argv[d->optind][0] != L'-' || argv[d->optind][1] == L'\0')
if (d->__nextchar == NULL || *d->__nextchar == L'\0') {
if (d->__last_nonopt > d->optind)
d->__last_nonopt = d->optind;
if (d->__first_nonopt > d->optind)
d->__first_nonopt = d->optind;
if (d->__ordering == PERMUTE) {
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
exchange_w(const_cast<wchar_t **>(argv), d);
else if (d->__last_nonopt != d->optind)
d->__first_nonopt = d->optind;
while (d->optind < argc && NONOPTION_P)
d->optind++;
d->__last_nonopt = d->optind;
}
if (d->optind != argc && !wcscmp(argv[d->optind], L"--")) {
d->optind++;
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
exchange_w(const_cast<wchar_t **>(argv), d);
else if (d->__first_nonopt == d->__last_nonopt)
d->__first_nonopt = d->optind;
d->__last_nonopt = argc;
d->optind = argc;
}
if (d->optind == argc) {
if (d->__first_nonopt != d->__last_nonopt)
d->optind = d->__first_nonopt;
return -1;
}
if (NONOPTION_P) {
if (d->__ordering == REQUIRE_ORDER)
return -1;
d->optarg = argv[d->optind++];
return 1;
}
if (longopts) {
if (argv[d->optind][1] == L'-') {
d->__nextchar = argv[d->optind] + 2;
return process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind, long_only, d, print_errors, L"--");
}
if (long_only && (argv[d->optind][2] || !wcschr(optstring, argv[d->optind][1]))) {
int code;
d->__nextchar = argv[d->optind] + 1;
code = process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind, long_only, d, print_errors, L"-");
if (code != -1)
return code;
}
}
d->__nextchar = argv[d->optind] + 1;
}
{
wchar_t c = *d->__nextchar++;
const wchar_t *temp = wcschr(optstring, c);
if (*d->__nextchar == L'\0')
++d->optind;
if (temp == NULL || c == L':' || c == L';') {
if (print_errors)
fwprintf(stderr, L"%s: invalid option -- '%c'\n", argv[0], c);
d->optopt = c;
return L'?';
}
if (temp[0] == L'W' && temp[1] == L';' && longopts != NULL) {
if (*d->__nextchar != L'\0')
d->optarg = d->__nextchar;
else if (d->optind == argc) {
if (print_errors)
fwprintf(stderr, L"%s: option requires an argument -- '%c'\n", argv[0], c);
d->optopt = c;
if (optstring[0] == L':')
c = L':';
else
c = L'?';
return c;
}
else
d->optarg = argv[d->optind];
d->__nextchar = d->optarg;
d->optarg = NULL;
return process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind,
0, d, print_errors, L"-W ");
}
if (temp[1] == L':') {
if (temp[2] == L':') {
if (*d->__nextchar != L'\0') {
d->optarg = d->__nextchar;
d->optind++;
}
else
d->optarg = NULL;
d->__nextchar = NULL;
}
else {
if (*d->__nextchar != L'\0') {
d->optarg = d->__nextchar;
d->optind++;
}
else if (d->optind == argc) {
if (print_errors)
fwprintf(stderr, L"%s: option requires an argument -- '%c'\n", argv[0], c);
d->optopt = c;
if (optstring[0] == L':')
c = L':';
else
c = L'?';
}
else
d->optarg = argv[d->optind++];
d->__nextchar = NULL;
}
}
return c;
}
}
int _getopt_internal_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, int posixly_correct);
int _getopt_internal_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, int posixly_correct) {
int result;
getopt_data_w.optind = optind;
getopt_data_w.opterr = opterr;
result = _getopt_internal_r_w(argc, argv, optstring, longopts, longind, long_only, &getopt_data_w, posixly_correct);
optind = getopt_data_w.optind;
optarg_w = getopt_data_w.optarg;
optopt = getopt_data_w.optopt;
return result;
}
int getopt_w(int argc, wchar_t *const *argv, const wchar_t *optstring) _GETOPT_THROW {
return _getopt_internal_w(argc, argv, optstring, static_cast<const struct option_w *>(0), static_cast<int *>(0), 0, 0);
}
int getopt_long_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW {
return _getopt_internal_w(argc, argv, options, long_options, opt_index, 0, 0);
}
int getopt_long_only_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW {
return _getopt_internal_w(argc, argv, options, long_options, opt_index, 1, 0);
}
int _getopt_long_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d);
int _getopt_long_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d) {
return _getopt_internal_r_w(argc, argv, options, long_options, opt_index, 0, d, 0);
}
int _getopt_long_only_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d);
int _getopt_long_only_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d) {
return _getopt_internal_r_w(argc, argv, options, long_options, opt_index, 1, d, 0);
}

View File

@@ -1,95 +1,135 @@
#ifndef __GETOPT_H__
/**
* DISCLAIMER
* This file has no copyright assigned and is placed in the Public Domain.
* This file is part of the mingw-w64 runtime package.
*
* The mingw-w64 runtime package and its code is distributed in the hope that it
* will be useful but WITHOUT ANY WARRANTY. ALL WARRANTIES, EXPRESSED OR
* IMPLIED ARE HEREBY DISCLAIMED. This includes but is not limited to
* warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
#define __GETOPT_H__
/* All the headers include this file. */
#include <crtdefs.h>
#ifdef __cplusplus
extern "C" {
#endif
extern int optind; /* index of first non-option in argv */
extern int optopt; /* single option character, as parsed */
extern int opterr; /* flag to enable built-in diagnostics... */
/* (user may set to zero, to suppress) */
extern char *optarg; /* pointer to argument of current option */
extern int getopt(int nargc, char * const *nargv, const char *options);
#ifdef _BSD_SOURCE
/*
* BSD adds the non-standard `optreset' feature, for reinitialisation
* of `getopt' parsing. We support this feature, for applications which
* proclaim their BSD heritage, before including this header; however,
* to maintain portability, developers are advised to avoid it.
*/
# define optreset __mingw_optreset
extern int optreset;
#endif
#ifdef __cplusplus
}
#endif
/*
* POSIX requires the `getopt' API to be specified in `unistd.h';
* thus, `unistd.h' includes this header. However, we do not want
* to expose the `getopt_long' or `getopt_long_only' APIs, when
* included in this manner. Thus, close the standard __GETOPT_H__
* declarations block, and open an additional __GETOPT_LONG_H__
* specific block, only when *not* __UNISTD_H_SOURCED__, in which
* to declare the extended API.
*/
#endif /* !defined(__GETOPT_H__) */
#if !defined(__UNISTD_H_SOURCED__) && !defined(__GETOPT_LONG_H__)
#define __GETOPT_LONG_H__
#ifdef __cplusplus
extern "C" {
#endif
struct option /* specification for a long form option... */
{
const char *name; /* option name, without leading hyphens */
int has_arg; /* does it take an argument? */
int *flag; /* where to save its status, or NULL */
int val; /* its associated status value */
};
enum /* permitted values for its `has_arg' field... */
{
no_argument = 0, /* option never takes an argument */
required_argument, /* option always requires an argument */
optional_argument /* option may take an argument */
};
extern int getopt_long(int nargc, char * const *nargv, const char *options,
const struct option *long_options, int *idx);
extern int getopt_long_only(int nargc, char * const *nargv, const char *options,
const struct option *long_options, int *idx);
/*
* Previous MinGW implementation had...
*/
#ifndef HAVE_DECL_GETOPT
/*
* ...for the long form API only; keep this for compatibility.
*/
# define HAVE_DECL_GETOPT 1
#endif
#ifdef __cplusplus
}
#endif
#endif /* !defined(__UNISTD_H_SOURCED__) && !defined(__GETOPT_LONG_H__) */
/* Getopt for Microsoft C
This code is a modification of the Free Software Foundation, Inc.
Getopt library for parsing command line argument the purpose was
to provide a Microsoft Visual C friendly derivative. This code
provides functionality for both Unicode and Multibyte builds.
Date: 02/03/2011 - Ludvik Jerabek - Initial Release
Version: 1.1
Comment: Supports getopt, getopt_long, and getopt_long_only
and POSIXLY_CORRECT environment flag
License: LGPL
Revisions:
02/03/2011 - Ludvik Jerabek - Initial Release
02/20/2011 - Ludvik Jerabek - Fixed compiler warnings at Level 4
07/05/2011 - Ludvik Jerabek - Added no_argument, required_argument, optional_argument defs
08/03/2011 - Ludvik Jerabek - Fixed non-argument runtime bug which caused runtime exception
08/09/2011 - Ludvik Jerabek - Added code to export functions for DLL and LIB
02/15/2012 - Ludvik Jerabek - Fixed _GETOPT_THROW definition missing in implementation file
08/01/2012 - Ludvik Jerabek - Created separate functions for char and wchar_t characters so single dll can do both unicode and ansi
10/15/2012 - Ludvik Jerabek - Modified to match latest GNU features
06/19/2015 - Ludvik Jerabek - Fixed maximum option limitation caused by option_a (255) and option_w (65535) structure val variable
09/24/2022 - Ludvik Jerabek - Updated to match most recent getopt release
09/25/2022 - Ludvik Jerabek - Fixed memory allocation (malloc call) issue for wchar_t*
**DISCLAIMER**
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING, BUT Not LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, OR NON-INFRINGEMENT. SOME JURISDICTIONS DO NOT ALLOW THE
EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT
APPLY TO YOU. IN NO EVENT WILL I BE LIABLE TO ANY PARTY FOR ANY
DIRECT, INDIRECT, SPECIAL OR OTHER CONSEQUENTIAL DAMAGES FOR ANY
USE OF THIS MATERIAL INCLUDING, WITHOUT LIMITATION, ANY LOST
PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR OTHER DATA ON
YOUR INFORMATION HANDLING SYSTEM OR OTHERWISE, EVEN If WE ARE
EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
*/
#ifndef __GETOPT_H_
#define __GETOPT_H_
#ifdef _GETOPT_API
# undef _GETOPT_API
#endif
#if defined(EXPORTS_GETOPT) && defined(STATIC_GETOPT)
# error "The preprocessor definitions of EXPORTS_GETOPT and STATIC_GETOPT can only be used individually"
#elif defined(STATIC_GETOPT)
# define _GETOPT_API
#elif defined(EXPORTS_GETOPT)
# pragma message("Exporting getopt library")
# define _GETOPT_API __declspec(dllexport)
#else
# pragma message("Importing getopt library")
# define _GETOPT_API __declspec(dllimport)
#endif
// Change behavior for C\C++
#ifdef __cplusplus
# define _BEGIN_EXTERN_C extern "C" {
# define _END_EXTERN_C }
# define _GETOPT_THROW throw()
#else
# define _BEGIN_EXTERN_C
# define _END_EXTERN_C
# define _GETOPT_THROW
#endif
// Standard GNU options
#define null_argument 0 /*Argument Null*/
#define no_argument 0 /*Argument Switch Only*/
#define required_argument 1 /*Argument Required*/
#define optional_argument 2 /*Argument Optional*/
// Shorter Options
#define ARG_NULL 0 /*Argument Null*/
#define ARG_NONE 0 /*Argument Switch Only*/
#define ARG_REQ 1 /*Argument Required*/
#define ARG_OPT 2 /*Argument Optional*/
#include <string.h>
#include <wchar.h>
_BEGIN_EXTERN_C
extern _GETOPT_API int optind;
extern _GETOPT_API int opterr;
extern _GETOPT_API int optopt;
// Ansi
struct option_a {
const char *name;
int has_arg;
int *flag;
int val;
};
extern _GETOPT_API char *optarg_a;
extern _GETOPT_API int getopt_a(int argc, char *const *argv, const char *optstring) _GETOPT_THROW;
extern _GETOPT_API int getopt_long_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW;
extern _GETOPT_API int getopt_long_only_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW;
// Unicode
struct option_w {
const wchar_t *name;
int has_arg;
int *flag;
int val;
};
extern _GETOPT_API wchar_t *optarg_w;
extern _GETOPT_API int getopt_w(int argc, wchar_t *const *argv, const wchar_t *optstring) _GETOPT_THROW;
extern _GETOPT_API int getopt_long_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW;
extern _GETOPT_API int getopt_long_only_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW;
_END_EXTERN_C
#undef _BEGIN_EXTERN_C
#undef _END_EXTERN_C
#undef _GETOPT_THROW
#undef _GETOPT_API
#ifdef _UNICODE
# define getopt getopt_w
# define getopt_long getopt_long_w
# define getopt_long_only getopt_long_only_w
# define option option_w
# define optarg optarg_w
#else
# define getopt getopt_a
# define getopt_long getopt_long_a
# define getopt_long_only getopt_long_only_a
# define option option_a
# define optarg optarg_a
#endif
#endif // __GETOPT_H_

View File

@@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.7)
set(SOURCES kdsingleapplication.cpp kdsingleapplication_localsocket.cpp)
set(HEADERS kdsingleapplication.h kdsingleapplication_localsocket_p.h)
qt_wrap_cpp(MOC ${HEADERS})
add_library(kdsingleapplication STATIC ${SOURCES} ${MOC})
target_compile_definitions(kdsingleapplication PRIVATE -DKDSINGLEAPPLICATION_STATIC_BUILD)
target_include_directories(kdsingleapplication PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(kdsingleapplication PUBLIC ${QtCore_LIBRARIES} ${QtNetwork_LIBRARIES} )

6
3rdparty/kdsingleapplication/LICENSE vendored Normal file
View File

@@ -0,0 +1,6 @@
KDSingleApplication is (C) 2019-2023, Klarälvdalens Datakonsult AB,
and is available under the terms of the MIT license.
See the full license text in the LICENSES folder.
Contact KDAB at <info@kdab.com> to inquire about commercial licensing.

53
3rdparty/kdsingleapplication/README.md vendored Normal file
View File

@@ -0,0 +1,53 @@
# KDSingleApplication
`KDSingleApplication` is a helper class for single-instance policy applications
written by [KDAB](https://www.kdab.com).
## Usage
Currently the documentation is woefully lacking, but see the examples or tests
for inspiration. Basically it involves:
1. Create a `Q(Core|Gui)Application` object.
2. Create a `KDSingleApplication` object.
3. Check if the current instance is *primary* (or "master") or
*secondary* (or "slave") by calling `isPrimaryInstance`:
* the *primary* instance needs to listen from messages coming from the
secondary instances, by connecting a slot to the `messageReceived` signal;
* the *secondary* instances can send messages to the primary instance
by calling `sendMessage`.
## Licensing
KDSingleApplication is (C) 2019-2023, Klarälvdalens Datakonsult AB, and is available
under the terms of the [MIT license](LICENSES/MIT.txt).
Contact KDAB at <info@kdab.com> if you need different licensing options.
## Get Involved
KDAB will happily accept external contributions.
Please submit your contributions or issue reports from our GitHub space at
<https://github.com/KDAB/KDSingleApplication>.
## About KDAB
KDSingleApplication is supported and maintained by Klarälvdalens Datakonsult AB (KDAB).
The KDAB Group is the global No.1 software consultancy for Qt, C++ and
OpenGL applications across desktop, embedded and mobile platforms.
The KDAB Group provides consulting and mentoring for developing Qt applications
from scratch and in porting from all popular and legacy frameworks to Qt.
We continue to help develop parts of Qt and are one of the major contributors
to the Qt Project. We can give advanced or standard trainings anywhere
around the globe on Qt as well as C++, OpenGL, 3D and more.
Please visit <https://www.kdab.com> to meet the people who write code like this.
Stay up-to-date with KDAB product announcements:
* [KDAB Newsletter](https://news.kdab.com)
* [KDAB Blogs](https://www.kdab.com/category/blogs)
* [KDAB on Twitter](https://twitter.com/KDABQt)

View File

@@ -0,0 +1,106 @@
/*
This file is part of KDSingleApplication.
SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@kdab.com> for commercial licensing options.
*/
#include "kdsingleapplication.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QFileInfo>
// TODO: make this pluggable.
#include "kdsingleapplication_localsocket_p.h"
// Avoiding dragging in Qt private APIs for now, so this does not inherit
// from QObjectPrivate.
class KDSingleApplicationPrivate
{
public:
explicit KDSingleApplicationPrivate(const QString &name, KDSingleApplication *q);
QString name() const
{
return m_name;
}
bool isPrimaryInstance() const
{
return m_impl.isPrimaryInstance();
}
bool sendMessage(const QByteArray &message, int timeout)
{
return m_impl.sendMessage(message, timeout);
}
private:
Q_DECLARE_PUBLIC(KDSingleApplication)
KDSingleApplication *q_ptr;
QString m_name;
KDSingleApplicationLocalSocket m_impl;
};
KDSingleApplicationPrivate::KDSingleApplicationPrivate(const QString &name, KDSingleApplication *q)
: q_ptr(q)
, m_name(name)
, m_impl(name)
{
if (Q_UNLIKELY(name.isEmpty()))
qFatal("KDSingleApplication requires a non-empty application name");
if (isPrimaryInstance()) {
QObject::connect(&m_impl, &KDSingleApplicationLocalSocket::messageReceived,
q, &KDSingleApplication::messageReceived);
}
}
static QString extractExecutableName(const QString &applicationFilePath)
{
return QFileInfo(applicationFilePath).fileName();
}
KDSingleApplication::KDSingleApplication(QObject *parent)
: KDSingleApplication(extractExecutableName(QCoreApplication::applicationFilePath()), parent)
{
}
KDSingleApplication::KDSingleApplication(const QString &name, QObject *parent)
: QObject(parent)
, d_ptr(new KDSingleApplicationPrivate(name, this))
{
}
QString KDSingleApplication::name() const
{
Q_D(const KDSingleApplication);
return d->name();
}
bool KDSingleApplication::isPrimaryInstance() const
{
Q_D(const KDSingleApplication);
return d->isPrimaryInstance();
}
bool KDSingleApplication::sendMessage(const QByteArray &message)
{
return sendMessageWithTimeout(message, 5000);
}
bool KDSingleApplication::sendMessageWithTimeout(const QByteArray &message, int timeout)
{
Q_ASSERT(!isPrimaryInstance());
Q_D(KDSingleApplication);
return d->sendMessage(message, timeout);
}
KDSingleApplication::~KDSingleApplication() = default;

View File

@@ -0,0 +1,48 @@
/*
This file is part of KDSingleApplication.
SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@kdab.com> for commercial licensing options.
*/
#ifndef KDSINGLEAPPLICATION_H
#define KDSINGLEAPPLICATION_H
#include <QtCore/QObject>
#include <memory>
#include "kdsingleapplication_lib.h"
class KDSingleApplicationPrivate;
class KDSINGLEAPPLICATION_EXPORT KDSingleApplication : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(bool isPrimaryInstance READ isPrimaryInstance CONSTANT)
public:
explicit KDSingleApplication(QObject *parent = nullptr);
explicit KDSingleApplication(const QString &name, QObject *parent = nullptr);
~KDSingleApplication();
QString name() const;
bool isPrimaryInstance() const;
public Q_SLOTS:
// avoid default arguments and overloads, as they don't mix with connections
bool sendMessage(const QByteArray &message);
bool sendMessageWithTimeout(const QByteArray &message, int timeout);
Q_SIGNALS:
void messageReceived(const QByteArray &message);
private:
Q_DECLARE_PRIVATE(KDSingleApplication)
std::unique_ptr<KDSingleApplicationPrivate> d_ptr;
};
#endif // KDSINGLEAPPLICATION_H

View File

@@ -0,0 +1,23 @@
/*
This file is part of KDSingleApplication.
SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@kdab.com> for commercial licensing options.
*/
#ifndef KDSINGLEAPPLICATION_LIB_H
#define KDSINGLEAPPLICATION_LIB_H
#include <QtCore/QtGlobal>
#if defined(KDSINGLEAPPLICATION_STATIC_BUILD)
#define KDSINGLEAPPLICATION_EXPORT
#elif defined(KDSINGLEAPPLICATION_SHARED_BUILD)
#define KDSINGLEAPPLICATION_EXPORT Q_DECL_EXPORT
#else
#define KDSINGLEAPPLICATION_EXPORT Q_DECL_IMPORT
#endif
#endif // KDSINGLEAPPLICATION_LIB_H

View File

@@ -0,0 +1,315 @@
/*
This file is part of KDSingleApplication.
SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@kdab.com> for commercial licensing options.
*/
#include "kdsingleapplication_localsocket_p.h"
#include <QtCore/QDir>
#include <QtCore/QDeadlineTimer>
#include <QtCore/QTimer>
#include <QtCore/QLockFile>
#include <QtCore/QDataStream>
#include <QtCore/QtDebug>
#include <QtCore/QLoggingCategory>
#include <QtNetwork/QLocalServer>
#include <QtNetwork/QLocalSocket>
#include <chrono>
#include <algorithm>
#ifdef Q_OS_UNIX
// for ::getuid()
# include <sys/types.h>
# include <unistd.h>
#endif
#ifdef Q_OS_WIN
# include <qt_windows.h>
#endif
namespace {
static constexpr auto LOCALSOCKET_CONNECTION_TIMEOUT = std::chrono::seconds(5);
static constexpr char LOCALSOCKET_PROTOCOL_VERSION = 2;
} // namespace
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-declarations"
#endif
Q_LOGGING_CATEGORY(kdsaLocalSocket, "kdsingleapplication.localsocket", QtWarningMsg)
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
KDSingleApplicationLocalSocket::KDSingleApplicationLocalSocket(const QString &name, QObject *parent)
: QObject(parent)
{
#if defined(Q_OS_UNIX)
/* cppcheck-suppress useInitializationList */
m_socketName = QStringLiteral("kdsingleapp-%1-%2")
.arg(::getuid())
.arg(name);
#elif defined(Q_OS_WIN)
// I'm not sure of a "global session identifier" on Windows; are
// multiple logins from the same user a possibility? For now, following this:
// https://docs.microsoft.com/en-us/windows/desktop/devnotes/getting-the-session-id-of-the-current-process
DWORD sessionId;
BOOL haveSessionId = ProcessIdToSessionId(GetCurrentProcessId(), &sessionId);
m_socketName = QString::fromUtf8("kdsingleapp-%1-%2")
.arg(haveSessionId ? sessionId : 0)
.arg(name);
#else
#error "KDSingleApplication has not been ported to this platform"
#endif
const QString lockFilePath =
QDir::tempPath() + QLatin1Char('/') + m_socketName + QLatin1String(".lock");
qCDebug(kdsaLocalSocket) << "Socket name is" << m_socketName;
qCDebug(kdsaLocalSocket) << "Lock file path is" << lockFilePath;
std::unique_ptr<QLockFile> lockFile(new QLockFile(lockFilePath));
lockFile->setStaleLockTime(0);
if (!lockFile->tryLock()) {
// someone else has the lock => we're secondary
qCDebug(kdsaLocalSocket) << "Secondary instance";
return;
}
qCDebug(kdsaLocalSocket) << "Primary instance";
std::unique_ptr<QLocalServer> server = std::make_unique<QLocalServer>();
if (!server->listen(m_socketName)) {
// maybe the primary crashed, leaving a stale socket; delete it and try again
QLocalServer::removeServer(m_socketName);
if (!server->listen(m_socketName)) {
// TODO: better error handling.
qWarning("KDSingleApplication: unable to make the primary instance listen on %ls: %ls",
qUtf16Printable(m_socketName),
qUtf16Printable(server->errorString()));
return;
}
}
connect(server.get(), &QLocalServer::newConnection,
this, &KDSingleApplicationLocalSocket::handleNewConnection);
m_lockFile = std::move(lockFile);
m_localServer = std::move(server);
}
KDSingleApplicationLocalSocket::~KDSingleApplicationLocalSocket() = default;
bool KDSingleApplicationLocalSocket::isPrimaryInstance() const
{
return m_localServer != nullptr;
}
bool KDSingleApplicationLocalSocket::sendMessage(const QByteArray &message, int timeout)
{
Q_ASSERT(!isPrimaryInstance());
QLocalSocket socket;
qCDebug(kdsaLocalSocket) << "Preparing to send message" << message << "with timeout" << timeout;
QDeadlineTimer deadline(timeout);
// There is an inherent race here with the setup of the server side.
// Even if the socket lock is held by the server, the server may not
// be listening yet. So this connection may fail; keep retrying
// until we hit the timeout.
do {
socket.connectToServer(m_socketName);
if (socket.waitForConnected(static_cast<int>(deadline.remainingTime())))
break;
} while (!deadline.hasExpired());
qCDebug(kdsaLocalSocket) << "Socket state:" << socket.state() << "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
if (deadline.hasExpired()) {
qCWarning(kdsaLocalSocket) << "Connection timed out";
return false;
}
socket.write(&LOCALSOCKET_PROTOCOL_VERSION, 1);
{
QByteArray encodedMessage;
QDataStream ds(&encodedMessage, QIODevice::WriteOnly);
ds << message;
socket.write(encodedMessage);
}
qCDebug(kdsaLocalSocket) << "Wrote message in the socket"
<< "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
// There is no acknowledgement mechanism here.
// Should there be one?
while (socket.bytesToWrite() > 0) {
if (!socket.waitForBytesWritten(static_cast<int>(deadline.remainingTime()))) {
qCWarning(kdsaLocalSocket) << "Message to primary timed out";
return false;
}
}
qCDebug(kdsaLocalSocket) << "Bytes written, now disconnecting"
<< "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
socket.disconnectFromServer();
if (socket.state() == QLocalSocket::UnconnectedState) {
qCDebug(kdsaLocalSocket) << "Disconnected -- success!";
return true;
}
if (!socket.waitForDisconnected(static_cast<int>(deadline.remainingTime()))) {
qCWarning(kdsaLocalSocket) << "Disconnection from primary timed out";
return false;
}
qCDebug(kdsaLocalSocket) << "Disconnected -- success!";
return true;
}
void KDSingleApplicationLocalSocket::handleNewConnection()
{
Q_ASSERT(m_localServer);
QLocalSocket *socket = nullptr;
while ((socket = m_localServer->nextPendingConnection())) {
qCDebug(kdsaLocalSocket) << "Got new connection on" << m_socketName << "state" << socket->state();
Connection c(socket);
socket = c.socket.get();
c.readDataConnection = QObjectConnectionHolder(
connect(socket, &QLocalSocket::readyRead,
this, &KDSingleApplicationLocalSocket::readDataFromSecondary));
c.secondaryDisconnectedConnection = QObjectConnectionHolder(
connect(socket, &QLocalSocket::disconnected,
this, &KDSingleApplicationLocalSocket::secondaryDisconnected));
c.abortConnection = QObjectConnectionHolder(
connect(c.timeoutTimer.get(), &QTimer::timeout,
this, &KDSingleApplicationLocalSocket::abortConnectionToSecondary));
m_clients.push_back(std::move(c));
// Note that by the time we get here, the socket could've already been closed,
// and no signals emitted (hello, Windows!). Read what's already in the socket.
if (readDataFromSecondarySocket(socket))
return;
if (socket->state() == QLocalSocket::UnconnectedState)
secondarySocketDisconnected(socket);
}
}
template<typename Container>
static auto findConnectionBySocket(Container &container, QLocalSocket *socket)
{
auto i = std::find_if(container.begin(),
container.end(),
[socket](const auto &c) { return c.socket.get() == socket; });
Q_ASSERT(i != container.end());
return i;
}
template<typename Container>
static auto findConnectionByTimer(Container &container, QTimer *timer)
{
auto i = std::find_if(container.begin(),
container.end(),
[timer](const auto &c) { return c.timeoutTimer.get() == timer; });
Q_ASSERT(i != container.end());
return i;
}
void KDSingleApplicationLocalSocket::readDataFromSecondary()
{
QLocalSocket *socket = static_cast<QLocalSocket *>(sender());
readDataFromSecondarySocket(socket);
}
bool KDSingleApplicationLocalSocket::readDataFromSecondarySocket(QLocalSocket *socket)
{
auto i = findConnectionBySocket(m_clients, socket);
Connection &c = *i;
c.readData.append(socket->readAll());
qCDebug(kdsaLocalSocket) << "Got more data from a secondary. Data read so far:" << c.readData;
const QByteArray &data = c.readData;
if (data.size() >= 1) {
if (data[0] != LOCALSOCKET_PROTOCOL_VERSION) {
qCDebug(kdsaLocalSocket) << "Got an invalid protocol version";
m_clients.erase(i);
return true;
}
}
QDataStream ds(data);
ds.skipRawData(1);
ds.startTransaction();
QByteArray message;
ds >> message;
if (ds.commitTransaction()) {
qCDebug(kdsaLocalSocket) << "Got a complete message:" << message;
Q_EMIT messageReceived(message);
m_clients.erase(i);
return true;
}
return false;
}
void KDSingleApplicationLocalSocket::secondaryDisconnected()
{
QLocalSocket *socket = static_cast<QLocalSocket *>(sender());
secondarySocketDisconnected(socket);
}
void KDSingleApplicationLocalSocket::secondarySocketDisconnected(QLocalSocket *socket)
{
auto i = findConnectionBySocket(m_clients, socket);
Connection c = std::move(*i);
m_clients.erase(i);
qCDebug(kdsaLocalSocket) << "Secondary disconnected. Data read:" << c.readData;
}
void KDSingleApplicationLocalSocket::abortConnectionToSecondary()
{
QTimer *timer = static_cast<QTimer *>(sender());
auto i = findConnectionByTimer(m_clients, timer);
Connection c = std::move(*i);
m_clients.erase(i);
qCDebug(kdsaLocalSocket) << "Secondary timed out. Data read:" << c.readData;
}
KDSingleApplicationLocalSocket::Connection::Connection(QLocalSocket *_socket)
: socket(_socket)
, timeoutTimer(new QTimer)
{
timeoutTimer->start(LOCALSOCKET_CONNECTION_TIMEOUT);
}

View File

@@ -0,0 +1,126 @@
/*
This file is part of KDSingleApplication.
SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@kdab.com> for commercial licensing options.
*/
#ifndef KDSINGLEAPPLICATION_LOCALSOCKET_P_H
#define KDSINGLEAPPLICATION_LOCALSOCKET_P_H
#include <QtCore/QObject>
#include <QtCore/QByteArray>
#include <QtCore/QString>
QT_BEGIN_NAMESPACE
class QLockFile;
class QLocalServer;
class QLocalSocket;
class QTimer;
QT_END_NAMESPACE
#include <memory>
#include <vector>
struct QObjectDeleteLater
{
void operator()(QObject *o)
{
o->deleteLater();
}
};
class QObjectConnectionHolder
{
Q_DISABLE_COPY(QObjectConnectionHolder)
QMetaObject::Connection c;
public:
QObjectConnectionHolder()
{
}
explicit QObjectConnectionHolder(QMetaObject::Connection _c)
: c(std::move(_c))
{
}
~QObjectConnectionHolder()
{
QObject::disconnect(c);
}
QObjectConnectionHolder(QObjectConnectionHolder &&other) noexcept
: c(std::exchange(other.c, {}))
{
}
QObjectConnectionHolder &operator=(QObjectConnectionHolder &&other) noexcept
{
QObjectConnectionHolder moved(std::move(other));
swap(moved);
return *this;
}
void swap(QObjectConnectionHolder &other) noexcept
{
using std::swap;
swap(c, other.c);
}
};
class KDSingleApplicationLocalSocket : public QObject
{
Q_OBJECT
public:
explicit KDSingleApplicationLocalSocket(const QString &name,
QObject *parent = nullptr);
~KDSingleApplicationLocalSocket();
bool isPrimaryInstance() const;
public Q_SLOTS:
bool sendMessage(const QByteArray &message, int timeout);
Q_SIGNALS:
void messageReceived(const QByteArray &message);
private:
void handleNewConnection();
void readDataFromSecondary();
bool readDataFromSecondarySocket(QLocalSocket *socket);
void secondaryDisconnected();
void secondarySocketDisconnected(QLocalSocket *socket);
void abortConnectionToSecondary();
QString m_socketName;
std::unique_ptr<QLockFile> m_lockFile; // protects m_localServer
std::unique_ptr<QLocalServer> m_localServer;
struct Connection
{
explicit Connection(QLocalSocket *s);
std::unique_ptr<QLocalSocket, QObjectDeleteLater> socket;
std::unique_ptr<QTimer, QObjectDeleteLater> timeoutTimer;
QByteArray readData;
// socket/timeoutTimer are deleted via deleteLater (as we delete them
// in slots connected to their signals). Before the deleteLater is acted upon,
// they may emit further signals, triggering logic that it's not supposed
// to be triggered (as the Connection has already been destroyed).
// Use this Holder to break the connections.
QObjectConnectionHolder readDataConnection;
QObjectConnectionHolder secondaryDisconnectedConnection;
QObjectConnectionHolder abortConnection;
};
std::vector<Connection> m_clients;
};
#endif // KDSINGLEAPPLICATION_LOCALSOCKET_P_H

View File

@@ -1,36 +0,0 @@
cmake_minimum_required(VERSION 3.7)
include(CheckIncludeFiles)
include(CheckFunctionExists)
check_function_exists(geteuid HAVE_GETEUID)
check_function_exists(getpwuid HAVE_GETPWUID)
set(SINGLEAPP-SOURCES singleapplication.cpp singleapplication_p.cpp)
set(SINGLEAPP-MOC-HEADERS singleapplication.h singleapplication_p.h)
qt_wrap_cpp(SINGLEAPP-SOURCES-MOC ${SINGLEAPP-MOC-HEADERS})
add_library(singleapplication STATIC ${SINGLEAPP-SOURCES} ${SINGLEAPP-SOURCES-MOC})
target_include_directories(singleapplication PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(singleapplication PRIVATE
${QtCore_LIBRARIES}
${QtWidgets_LIBRARIES}
${QtNetwork_LIBRARIES}
)
set(SINGLECOREAPP-SOURCES singlecoreapplication.cpp singlecoreapplication_p.cpp)
set(SINGLECOREAPP-MOC-HEADERS singlecoreapplication.h singlecoreapplication_p.h)
qt_wrap_cpp(SINGLECOREAPP-SOURCES-MOC ${SINGLECOREAPP-MOC-HEADERS})
add_library(singlecoreapplication STATIC ${SINGLECOREAPP-SOURCES} ${SINGLECOREAPP-SOURCES-MOC})
target_include_directories(singlecoreapplication PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(singlecoreapplication PRIVATE
${QtCore_LIBRARIES}
${QtNetwork_LIBRARIES}
)
configure_file(config.h.in "${CMAKE_CURRENT_BINARY_DIR}/config.h")

View File

@@ -1,24 +0,0 @@
The MIT License (MIT)
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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Note: Some of the examples include code not distributed under the terms of the
MIT License.

View File

@@ -1,305 +0,0 @@
SingleApplication
=================
[![CI](https://github.com/itay-grudev/SingleApplication/workflows/CI:%20Build%20Test/badge.svg)](https://github.com/itay-grudev/SingleApplication/actions)
This is a replacement of the QtSingleApplication for `Qt5` and `Qt6`.
Keeps the Primary Instance of your Application and kills each subsequent
instances. It can (if enabled) spawn secondary (non-related to the primary)
instances and can send data to the primary instance from secondary instances.
Usage
-----
The `SingleApplication` class inherits from whatever `Q[Core|Gui]Application`
class you specify via the `QAPPLICATION_CLASS` macro (`QCoreApplication` is the
default). Further usage is similar to the use of the `Q[Core|Gui]Application`
classes.
You can use the library as if you use any other `QCoreApplication` derived
class:
```cpp
#include <QApplication>
#include <SingleApplication.h>
int main( int argc, char* argv[] )
{
SingleApplication app( argc, argv );
return app.exec();
}
```
To include the library files I would recommend that you add it as a git
submodule to your project. Here is how:
```bash
git submodule add https://github.com/itay-grudev/SingleApplication.git singleapplication
```
**Qmake:**
Then include the `singleapplication.pri` file in your `.pro` project file.
```qmake
include(singleapplication/singleapplication.pri)
DEFINES += QAPPLICATION_CLASS=QApplication
```
**CMake:**
Then include the subdirectory in your `CMakeLists.txt` project file.
```cmake
set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication")
add_subdirectory(src/third-party/singleapplication)
target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication)
```
The library sets up a `QLocalServer` and a `QSharedMemory` block. The first
instance of your Application is your Primary Instance. It would check if the
shared memory block exists and if not it will start a `QLocalServer` and listen
for connections. Each subsequent instance of your application would check if the
shared memory block exists and if it does, it will connect to the QLocalServer
to notify the primary instance that a new instance had been started, after which
it would terminate with status code `0`. In the Primary Instance
`SingleApplication` would emit the `instanceStarted()` signal upon detecting
that a new instance had been started.
The library uses `stdlib` to terminate the program with the `exit()` function.
Also don't forget to specify which `QCoreApplication` class your app is using if it
is not `QCoreApplication` as in examples above.
The `Instance Started` signal
-----------------------------
The SingleApplication class implements a `instanceStarted()` signal. You can
bind to that signal to raise your application's window when a new instance had
been started, for example.
```cpp
// window is a QWindow instance
QObject::connect(
&app,
&SingleApplication::instanceStarted,
&window,
&QWindow::raise
);
```
Using `SingleApplication::instance()` is a neat way to get the
`SingleApplication` instance for binding to it's signals anywhere in your
program.
__Note:__ On Windows the ability to bring the application windows to the
foreground is restricted. See [Windows specific implementations](Windows.md)
for a workaround and an example implementation.
Secondary Instances
-------------------
If you want to be able to launch additional Secondary Instances (not related to
your Primary Instance) you have to enable that with the third parameter of the
`SingleApplication` constructor. The default is `false` meaning no Secondary
Instances. Here is an example of how you would start a Secondary Instance send
a message with the command line arguments to the primary instance and then shut
down.
```cpp
int main(int argc, char *argv[])
{
SingleApplication app( argc, argv, true );
if( app.isSecondary() ) {
app.sendMessage( app.arguments().join(' ')).toUtf8() );
app.exit( 0 );
}
return app.exec();
}
```
*__Note:__ A secondary instance won't cause the emission of the
`instanceStarted()` signal by default. See `SingleApplication::Mode` for more
details.*
You can check whether your instance is a primary or secondary with the following
methods:
```cpp
app.isPrimary();
// or
app.isSecondary();
```
*__Note:__ If your Primary Instance is terminated a newly launched instance
will replace the Primary one even if the Secondary flag has been set.*
Examples
--------
There are three examples provided in this repository:
* Basic example that prevents a secondary instance from starting [`examples/basic`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/basic)
* An example of a graphical application raising it's parent window [`examples/calculator`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/calculator)
* A console application sending the primary instance it's command line parameters [`examples/sending_arguments`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/sending_arguments)
API
---
### Members
```cpp
SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 100, QString userData = QString() )
```
Depending on whether `allowSecondary` is set, this constructor may terminate
your app if there is already a primary instance running. Additional `Options`
can be specified to set whether the SingleApplication block should work
user-wide or system-wide. Additionally the `Mode::SecondaryNotification` may be
used to notify the primary instance whenever a secondary instance had been
started (disabled by default). `timeout` specifies the maximum time in
milliseconds to wait for blocking operations. Setting `userData` provides additional data that will isolate this instance from other instances that do not have the same (or any) user data set.
*__Note:__ `argc` and `argv` may be changed as Qt removes arguments that it
recognizes.*
*__Note:__ `Mode::SecondaryNotification` only works if set on both the primary
and the secondary instance.*
*__Note:__ Operating system can restrict the shared memory blocks to the same
user, in which case the User/System modes will have no effect and the block will
be user wide.*
---
```cpp
bool SingleApplication::sendMessage( QByteArray message, int timeout = 100 )
```
Sends `message` to the Primary Instance. Uses `timeout` as a the maximum timeout
in milliseconds for blocking functions. Returns `true` if the message has been sent
successfully. If the message can't be sent or the function timeouts - returns `false`.
---
```cpp
bool SingleApplication::isPrimary()
```
Returns if the instance is the primary instance.
---
```cpp
bool SingleApplication::isSecondary()
```
Returns if the instance is a secondary instance.
---
```cpp
quint32 SingleApplication::instanceId()
```
Returns a unique identifier for the current instance.
---
```cpp
qint64 SingleApplication::primaryPid()
```
Returns the process ID (PID) of the primary instance.
---
```cpp
QString SingleApplication::primaryUser()
```
Returns the username the primary instance is running as.
---
```cpp
QString SingleApplication::currentUser()
```
Returns the username the current instance is running as.
### Signals
```cpp
void SingleApplication::instanceStarted()
```
Triggered whenever a new instance had been started, except for secondary
instances if the `Mode::SecondaryNotification` flag is not specified.
---
```cpp
void SingleApplication::receivedMessage( quint32 instanceId, QByteArray message )
```
Triggered whenever there is a message received from a secondary instance.
---
### Flags
```cpp
enum SingleApplication::Mode
```
* `Mode::User` - The SingleApplication block should apply user wide. This adds
user specific data to the key used for the shared memory and server name.
This is the default functionality.
* `Mode::System` The SingleApplication block applies system-wide.
* `Mode::SecondaryNotification` Whether to trigger `instanceStarted()` even
whenever secondary instances are started.
* `Mode::ExcludeAppPath` Excludes the application path from the server name
(and memory block) hash.
* `Mode::ExcludeAppVersion` Excludes the application version from the server
name (and memory block) hash.
*__Note:__ `Mode::SecondaryNotification` only works if set on both the primary
and the secondary instance.*
*__Note:__ Operating system can restrict the shared memory blocks to the same
user, in which case the User/System modes will have no effect and the block will
be user wide.*
---
Versioning
----------
Each major version introduces either very significant changes or is not
backwards compatible with the previous version. Minor versions only add
additional features, bug fixes or performance improvements and are backwards
compatible with the previous release. See [`CHANGELOG.md`](CHANGELOG.md) for
more details.
Implementation
--------------
The library is implemented with a QSharedMemory block which is thread safe and
guarantees a race condition will not occur. It also uses a QLocalSocket to
notify the main process that a new instance had been spawned and thus invoke the
`instanceStarted()` signal and for messaging the primary instance.
Additionally the library can recover from being forcefully killed on *nix
systems and will reset the memory block given that there are no other
instances running.
License
-------
This library and it's supporting documentation are released under
`The MIT License (MIT)` with the exception of the Qt calculator examples which
is distributed under the BSD license.

View File

@@ -1,2 +0,0 @@
#cmakedefine HAVE_GETEUID
#cmakedefine HAVE_GETPWUID

View File

@@ -1,266 +0,0 @@
// The MIT License (MIT)
//
// 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
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#include <cstdlib>
#include <limits>
#include <QtGlobal>
#include <QApplication>
#include <QThread>
#include <QSharedMemory>
#include <QLocalSocket>
#include <QByteArray>
#include <QElapsedTimer>
#include <QtDebug>
#include "singleapplication.h"
#include "singleapplication_p.h"
/**
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
* @param argc
* @param argv
* @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[], const bool allowSecondary, const Options options, const int timeout)
: app_t(argc, argv),
d_ptr(new SingleApplicationPrivate(this)) {
Q_D(SingleApplication);
// Store the current mode of the program
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
SingleApplicationPrivate::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_;
#endif
// Guarantee thread safe behaviour with a shared memory block.
d->memory_ = new QSharedMemory(d->blockServerName_);
// Create a shared memory block
if (d->memory_->create(sizeof(InstancesInfo))) {
// Initialize the shared memory block
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory block after create.";
abortSafely();
}
d->initializeMemoryBlock();
}
else {
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 *instance = static_cast<InstancesInfo*>(d->memory_->data());
QElapsedTimer time;
time.start();
// Make sure the shared memory block is initialised and in consistent state
forever {
// If the shared memory block's checksum is valid continue
if (d->blockChecksum() == instance->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();
}
// 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();
}
SingleApplicationPrivate::randomSleep();
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory after random wait.";
abortSafely();
}
}
if (!instance->primary) {
d->startPrimary();
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) {
d->startSecondary();
if (d->options_ & Mode::SecondaryNotification) {
d->connectToPrimary(timeout, SingleApplicationPrivate::SecondaryInstance);
}
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory after secondary start.";
qDebug() << d->memory_->errorString();
}
return;
}
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory at end of execution.";
qDebug() << d->memory_->errorString();
}
d->connectToPrimary(timeout, SingleApplicationPrivate::NewInstance);
delete d;
::exit(EXIT_SUCCESS);
}
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() const {
Q_D(const SingleApplication);
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() const {
Q_D(const SingleApplication);
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() const {
Q_D(const SingleApplication);
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() const {
Q_D(const 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() const {
Q_D(const 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() const {
return SingleApplicationPrivate::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 successfully, false otherwise.
*/
bool SingleApplication::sendMessage(const QByteArray &message, const int timeout) {
Q_D(SingleApplication);
// Nobody to connect to
if (isPrimary()) return false;
// Make sure the socket is connected
if (!d->connectToPrimary(timeout, SingleApplicationPrivate::Reconnect)) {
return false;
}
return d->writeConfirmedMessage(timeout, message);
}
/**
* 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,152 +0,0 @@
// The MIT License (MIT)
//
// 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
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#ifndef SINGLEAPPLICATION_H
#define SINGLEAPPLICATION_H
#include <QtGlobal>
#include <QApplication>
#include <QFlags>
#include <QByteArray>
class SingleApplicationPrivate;
/**
* @brief The SingleApplication class handles multiple instances of the same Application
* @see QApplication
*/
class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-parent-argument
Q_OBJECT
using app_t = QApplication;
public:
/**
* @brief Mode of operation of SingleApplication.
* Whether the block should be user-wide or system-wide and whether the
* primary instance should be notified when a secondary instance had been
* started.
* @note Operating system can restrict the shared memory blocks to the same
* user, in which case the User/System modes will have no effect and the
* block will be user wide.
* @enum
*/
enum Mode {
User = 1 << 0,
System = 1 << 1,
SecondaryNotification = 1 << 2,
ExcludeAppVersion = 1 << 3,
ExcludeAppPath = 1 << 4
};
Q_DECLARE_FLAGS(Options, Mode)
/**
* @brief Intitializes a SingleApplication instance with argc command line
* arguments in argv
* @arg {int &} argc - Number of arguments in argv
* @arg {const char *[]} argv - Supplied command line arguments
* @arg {bool} allowSecondary - Whether to start the instance as secondary
* if there is already a primary instance.
* @arg {Mode} mode - Whether for the SingleApplication block to be applied
* User wide or System wide.
* @arg {int} timeout - Timeout to wait in milliseconds.
* @note argc and argv may be changed as Qt removes arguments that it
* recognizes
* @note Mode::SecondaryNotification only works if set on both the primary
* instance and the secondary instance.
* @note The timeout is just a hint for the maximum time of blocking
* operations. It does not guarantee that the SingleApplication
* initialisation will be completed in given time, though is a good hint.
* Usually 4*timeout would be the worst case (fail) scenario.
*/
explicit SingleApplication(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
~SingleApplication() override;
/**
* @brief Returns if the instance is the primary instance
* @returns {bool}
*/
bool isPrimary() const;
/**
* @brief Returns if the instance is a secondary instance
* @returns {bool}
*/
bool isSecondary() const;
/**
* @brief Returns a unique identifier for the current instance
* @returns {qint32}
*/
quint32 instanceId() const;
/**
* @brief Returns the process ID (PID) of the primary instance
* @returns {qint64}
*/
qint64 primaryPid() const;
/**
* @brief Returns the username of the user running the primary instance
* @returns {QString}
*/
QString primaryUser() const;
/**
* @brief Returns the username of the current user
* @returns {QString}
*/
QString currentUser() const;
/**
* @brief Sends a message to the primary instance. Returns true on success.
* @param {int} timeout - Timeout for connecting
* @returns {bool}
* @note sendMessage() will return false if invoked from the primary
* instance.
*/
bool sendMessage(const QByteArray &message, const int timeout = 1000);
signals:
void instanceStarted();
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

View File

@@ -1,526 +0,0 @@
// The MIT License (MIT)
//
// 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
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#include "config.h"
#include <QtGlobal>
#include <cstdlib>
#include <cstddef>
#ifdef Q_OS_UNIX
# include <unistd.h>
# include <sys/types.h>
# include <pwd.h>
#endif
#ifdef Q_OS_WIN
# ifndef NOMINMAX
# define NOMINMAX 1
# endif
# include <windows.h>
# include <lmcons.h>
#endif
#include <QObject>
#include <QThread>
#include <QIODevice>
#include <QSharedMemory>
#include <QByteArray>
#include <QDataStream>
#include <QCryptographicHash>
#include <QLocalServer>
#include <QLocalSocket>
#include <QElapsedTimer>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#else
# include <QDateTime>
#endif
#include "singleapplication.h"
#include "singleapplication_p.h"
SingleApplicationPrivate::SingleApplicationPrivate(SingleApplication *ptr)
: q_ptr(ptr),
memory_(nullptr),
socket_(nullptr),
server_(nullptr),
instanceNumber_(-1) {}
SingleApplicationPrivate::~SingleApplicationPrivate() {
if (socket_ != nullptr) {
socket_->close();
delete socket_;
socket_ = nullptr;
}
if (memory_ != nullptr) {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
if (server_ != nullptr) {
server_->close();
delete server_;
instance->primary = false;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
memory_->unlock();
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
}
void SingleApplicationPrivate::genBlockServerName() {
QCryptographicHash appData(QCryptographicHash::Sha256);
appData.addData("SingleApplication");
appData.addData(SingleApplication::app_t::applicationName().toUtf8());
appData.addData(SingleApplication::app_t::organizationName().toUtf8());
appData.addData(SingleApplication::app_t::organizationDomain().toUtf8());
if (!(options_ & SingleApplication::Mode::ExcludeAppVersion)) {
appData.addData(SingleApplication::app_t::applicationVersion().toUtf8());
}
if (!(options_ & SingleApplication::Mode::ExcludeAppPath)) {
#if defined(Q_OS_UNIX)
const QByteArray appImagePath = qgetenv("APPIMAGE");
if (appImagePath.isEmpty()) {
appData.addData(SingleApplication::app_t::applicationFilePath().toUtf8());
}
else {
appData.addData(appImagePath);
};
#elif defined(Q_OS_WIN)
appData.addData(SingleApplication::app_t::applicationFilePath().toLower().toUtf8());
#else
appData.addData(SingleApplication::app_t::applicationFilePath().toUtf8());
#endif
}
// User level block requires a user specific data in the hash
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("/", "_");
}
void SingleApplicationPrivate::initializeMemoryBlock() const {
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = false;
instance->secondary = 0;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
void SingleApplicationPrivate::startPrimary() {
// Reset the number of connections
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = true;
instance->primaryPid = QCoreApplication::applicationPid();
qstrncpy(instance->primaryUser, getUsername().toUtf8().data(), sizeof(instance->primaryUser));
instance->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();
// 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() {
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->secondary += 1;
instance->checksum = blockChecksum();
instanceNumber_ = instance->secondary;
}
bool SingleApplicationPrivate::connectToPrimary(const int timeout, const 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_->state() == QLocalSocket::ConnectedState) return true;
if (socket_->state() != QLocalSocket::ConnectedState) {
forever {
randomSleep();
if (socket_->state() != QLocalSocket::ConnectingState) {
socket_->connectToServer(blockServerName_);
}
if (socket_->state() == QLocalSocket::ConnectingState) {
socket_->waitForConnected(static_cast<int>(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
QByteArray initMsg;
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
writeStream.setVersion(QDataStream::Qt_5_8);
writeStream << blockServerName_.toLatin1();
writeStream << static_cast<quint8>(connectionType);
writeStream << instanceNumber_;
#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
writeStream << checksum;
return writeConfirmedMessage(static_cast<int>(timeout - time.elapsed()), initMsg);
}
void SingleApplicationPrivate::writeAck(QLocalSocket *sock) {
sock->putChar('\n');
}
bool SingleApplicationPrivate::writeConfirmedMessage(const int timeout, const QByteArray &msg) const {
QElapsedTimer time;
time.start();
// Frame 1: 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>(msg.length());
if (!writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), header)) {
return false;
}
// Frame 2: The message
return writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), msg);
}
bool SingleApplicationPrivate::writeConfirmedFrame(const int timeout, const QByteArray &msg) const {
socket_->write(msg);
socket_->flush();
bool result = socket_->waitForReadyRead(timeout);
if (result) {
socket_->read(1);
return true;
}
return false;
}
quint16 SingleApplicationPrivate::blockChecksum() const {
#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() const {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
qint64 pid = instance->primaryPid;
memory_->unlock();
return pid;
}
QString SingleApplicationPrivate::primaryUser() const {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
QByteArray username = instance->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());
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [nextConnSocket, this]() {
const ConnectionInfo &info = connectionMap_[nextConnSocket];
slotClientConnectionClosed(nextConnSocket, info.instanceId);
});
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [nextConnSocket, this]() {
connectionMap_.remove(nextConnSocket);
});
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [nextConnSocket, this]() {
const ConnectionInfo &info = connectionMap_[nextConnSocket];
switch (info.stage) {
case StageInitHeader:
readMessageHeader(nextConnSocket, StageInitBody);
break;
case StageInitBody:
readInitMessageBody(nextConnSocket);
break;
case StageConnectedHeader:
readMessageHeader(nextConnSocket, StageConnectedBody);
break;
case StageConnectedBody:
this->slotDataAvailable(nextConnSocket, info.instanceId);
break;
default:
break;
};
});
}
void SingleApplicationPrivate::readMessageHeader(QLocalSocket *sock, const SingleApplicationPrivate::ConnectionStage nextStage) {
if (!connectionMap_.contains(sock)) {
return;
}
if (sock->bytesAvailable() < static_cast<qint64>(sizeof(quint64))) {
return;
}
QDataStream headerStream(sock);
headerStream.setVersion(QDataStream::Qt_5_8);
// Read the header to know the message length
quint64 msgLen = 0;
headerStream >> msgLen;
ConnectionInfo &info = connectionMap_[sock];
info.stage = nextStage;
info.msgLen = msgLen;
writeAck(sock);
}
bool SingleApplicationPrivate::isFrameComplete(QLocalSocket *sock) {
if (!connectionMap_.contains(sock)) {
return false;
}
const ConnectionInfo &info = connectionMap_[sock];
return (sock->bytesAvailable() >= static_cast<qint64>(info.msgLen));
}
void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
Q_Q(SingleApplication);
if (!isFrameComplete(sock)) {
return;
}
// Read the message body
QByteArray msgBytes = sock->readAll();
QDataStream readStream(msgBytes);
readStream.setVersion(QDataStream::Qt_5_8);
// server name
QByteArray latin1Name;
readStream >> latin1Name;
// connection type
ConnectionType connectionType = InvalidConnection;
quint8 connTypeVal = InvalidConnection;
readStream >> connTypeVal;
connectionType = static_cast<ConnectionType>(connTypeVal);
// instance id
quint32 instanceId = 0;
readStream >> instanceId;
// checksum
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;
if (!isValid) {
sock->close();
return;
}
ConnectionInfo &info = connectionMap_[sock];
info.instanceId = instanceId;
info.stage = StageConnectedHeader;
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleApplication::Mode::SecondaryNotification)) {
emit q->instanceStarted();
}
writeAck(sock);
}
void SingleApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
Q_Q(SingleApplication);
if (!isFrameComplete(dataSocket)) {
return;
}
const QByteArray message = dataSocket->readAll();
writeAck(dataSocket);
ConnectionInfo &info = connectionMap_[dataSocket];
info.stage = StageConnectedHeader;
emit q->receivedMessage(instanceId, message);
}
void SingleApplicationPrivate::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
if (closedSocket->bytesAvailable() > 0) {
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(qrand() % 11 + 8);
#endif
}

View File

@@ -1,116 +0,0 @@
// The MIT License (MIT)
//
// 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
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#ifndef SINGLEAPPLICATION_P_H
#define SINGLEAPPLICATION_P_H
#include <QtGlobal>
#include <QObject>
#include <QString>
#include <QHash>
#include "singleapplication.h"
class QLocalServer;
class QLocalSocket;
class QSharedMemory;
struct InstancesInfo {
bool primary;
quint32 secondary;
qint64 primaryPid;
char primaryUser[128];
quint16 checksum;
};
struct ConnectionInfo {
explicit ConnectionInfo() : msgLen(0), instanceId(0), stage(0) {}
quint64 msgLen;
quint32 instanceId;
quint8 stage;
};
class SingleApplicationPrivate : public QObject {
Q_OBJECT
public:
enum ConnectionType : quint8 {
InvalidConnection = 0,
NewInstance = 1,
SecondaryInstance = 2,
Reconnect = 3
};
enum ConnectionStage : quint8 {
StageInitHeader = 0,
StageInitBody = 1,
StageConnectedHeader = 2,
StageConnectedBody = 3,
};
Q_DECLARE_PUBLIC(SingleApplication)
explicit SingleApplicationPrivate(SingleApplication *ptr);
~SingleApplicationPrivate() override;
static QString getUsername();
void genBlockServerName();
void initializeMemoryBlock() const;
void startPrimary();
void startSecondary();
bool connectToPrimary(const int timeout, const ConnectionType connectionType);
quint16 blockChecksum() const;
qint64 primaryPid() const;
QString primaryUser() const;
bool isFrameComplete(QLocalSocket *sock);
void readMessageHeader(QLocalSocket *socket, const ConnectionStage nextStage);
void readInitMessageBody(QLocalSocket *socket);
void writeAck(QLocalSocket *sock);
bool writeConfirmedFrame(const int timeout, const QByteArray &msg) const;
bool writeConfirmedMessage(const int timeout, const QByteArray &msg) const;
static void randomSleep();
SingleApplication *q_ptr;
QSharedMemory *memory_;
QLocalSocket *socket_;
QLocalServer *server_;
quint32 instanceNumber_;
QString blockServerName_;
SingleApplication::Options options_;
QHash<QLocalSocket*, ConnectionInfo> connectionMap_;
public slots:
void slotConnectionEstablished();
void slotDataAvailable(QLocalSocket*, const quint32);
void slotClientConnectionClosed(QLocalSocket*, const quint32);
};
#endif // SINGLEAPPLICATION_P_H

View File

@@ -1,266 +0,0 @@
// The MIT License (MIT)
//
// 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
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#include <cstdlib>
#include <limits>
#include <QtGlobal>
#include <QCoreApplication>
#include <QThread>
#include <QSharedMemory>
#include <QLocalSocket>
#include <QByteArray>
#include <QElapsedTimer>
#include <QtDebug>
#include "singlecoreapplication.h"
#include "singlecoreapplication_p.h"
/**
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
* @param argc
* @param argv
* @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[], const bool allowSecondary, const Options options, const int timeout)
: app_t(argc, argv),
d_ptr(new SingleCoreApplicationPrivate(this)) {
Q_D(SingleCoreApplication);
// Store the current mode of the program
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
SingleCoreApplicationPrivate::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_;
#endif
// Guarantee thread safe behaviour with a shared memory block.
d->memory_ = new QSharedMemory(d->blockServerName_);
// Create a shared memory block
if (d->memory_->create(sizeof(InstancesInfo))) {
// Initialize the shared memory block
if (!d->memory_->lock()) {
qCritical() << "SingleCoreApplication: Unable to lock memory block after create.";
abortSafely();
}
d->initializeMemoryBlock();
}
else {
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 *instance = static_cast<InstancesInfo*>(d->memory_->data());
QElapsedTimer time;
time.start();
// Make sure the shared memory block is initialised and in consistent state
forever {
// If the shared memory block's checksum is valid continue
if (d->blockChecksum() == instance->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();
}
// 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();
}
SingleCoreApplicationPrivate::randomSleep();
if (!d->memory_->lock()) {
qCritical() << "SingleCoreApplication: Unable to lock memory after random wait.";
abortSafely();
}
}
if (!instance->primary) {
d->startPrimary();
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) {
d->startSecondary();
if (d->options_ & Mode::SecondaryNotification) {
d->connectToPrimary(timeout, SingleCoreApplicationPrivate::SecondaryInstance);
}
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory after secondary start.";
qDebug() << d->memory_->errorString();
}
return;
}
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory at end of execution.";
qDebug() << d->memory_->errorString();
}
d->connectToPrimary(timeout, SingleCoreApplicationPrivate::NewInstance);
delete d;
::exit(EXIT_SUCCESS);
}
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() const {
Q_D(const SingleCoreApplication);
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() const {
Q_D(const SingleCoreApplication);
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() const {
Q_D(const SingleCoreApplication);
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() const {
Q_D(const 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() const {
Q_D(const 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() const {
return SingleCoreApplicationPrivate::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 successfully, false otherwise.
*/
bool SingleCoreApplication::sendMessage(const QByteArray &message, const int timeout) {
Q_D(SingleCoreApplication);
// Nobody to connect to
if (isPrimary()) return false;
// Make sure the socket is connected
if (!d->connectToPrimary(timeout, SingleCoreApplicationPrivate::Reconnect)) {
return false;
}
return d->writeConfirmedMessage(timeout, message);
}
/**
* 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,152 +0,0 @@
// The MIT License (MIT)
//
// 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
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#ifndef SINGLECOREAPPLICATION_H
#define SINGLECOREAPPLICATION_H
#include <QtGlobal>
#include <QCoreApplication>
#include <QFlags>
#include <QByteArray>
class SingleCoreApplicationPrivate;
/**
* @brief The SingleCoreApplication class handles multiple instances of the same Application
* @see QCoreApplication
*/
class SingleCoreApplication : public QCoreApplication { // clazy:exclude=ctor-missing-parent-argument
Q_OBJECT
using app_t = QCoreApplication;
public:
/**
* @brief Mode of operation of SingleCoreApplication.
* Whether the block should be user-wide or system-wide and whether the
* primary instance should be notified when a secondary instance had been
* started.
* @note Operating system can restrict the shared memory blocks to the same
* user, in which case the User/System modes will have no effect and the
* block will be user wide.
* @enum
*/
enum Mode {
User = 1 << 0,
System = 1 << 1,
SecondaryNotification = 1 << 2,
ExcludeAppVersion = 1 << 3,
ExcludeAppPath = 1 << 4
};
Q_DECLARE_FLAGS(Options, Mode)
/**
* @brief Intitializes a SingleCoreApplication instance with argc command line
* arguments in argv
* @arg {int &} argc - Number of arguments in argv
* @arg {const char *[]} argv - Supplied command line arguments
* @arg {bool} allowSecondary - Whether to start the instance as secondary
* if there is already a primary instance.
* @arg {Mode} mode - Whether for the SingleCoreApplication block to be applied
* User wide or System wide.
* @arg {int} timeout - Timeout to wait in milliseconds.
* @note argc and argv may be changed as Qt removes arguments that it
* recognizes
* @note Mode::SecondaryNotification only works if set on both the primary
* instance and the secondary instance.
* @note The timeout is just a hint for the maximum time of blocking
* 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.
*/
explicit SingleCoreApplication(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
~SingleCoreApplication() override;
/**
* @brief Returns if the instance is the primary instance
* @returns {bool}
*/
bool isPrimary() const;
/**
* @brief Returns if the instance is a secondary instance
* @returns {bool}
*/
bool isSecondary() const;
/**
* @brief Returns a unique identifier for the current instance
* @returns {qint32}
*/
quint32 instanceId() const;
/**
* @brief Returns the process ID (PID) of the primary instance
* @returns {qint64}
*/
qint64 primaryPid() const;
/**
* @brief Returns the username of the user running the primary instance
* @returns {QString}
*/
QString primaryUser() const;
/**
* @brief Returns the username of the current user
* @returns {QString}
*/
QString currentUser() const;
/**
* @brief Sends a message to the primary instance. Returns true on success.
* @param {int} timeout - Timeout for connecting
* @returns {bool}
* @note sendMessage() will return false if invoked from the primary
* instance.
*/
bool sendMessage(const QByteArray &message, const int timeout = 1000);
signals:
void instanceStarted();
void receivedMessage(quint32 instanceId, QByteArray message);
private:
SingleCoreApplicationPrivate *d_ptr;
Q_DECLARE_PRIVATE(SingleCoreApplication)
void abortSafely();
};
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleCoreApplication::Options)
#endif // SINGLECOREAPPLICATION_H

View File

@@ -1,526 +0,0 @@
// The MIT License (MIT)
//
// 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
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#include "config.h"
#include <QtGlobal>
#include <cstdlib>
#include <cstddef>
#ifdef Q_OS_UNIX
# include <unistd.h>
# include <sys/types.h>
# include <pwd.h>
#endif
#ifdef Q_OS_WIN
# ifndef NOMINMAX
# define NOMINMAX 1
# endif
# include <windows.h>
# include <lmcons.h>
#endif
#include <QObject>
#include <QThread>
#include <QIODevice>
#include <QSharedMemory>
#include <QByteArray>
#include <QDataStream>
#include <QCryptographicHash>
#include <QLocalServer>
#include <QLocalSocket>
#include <QElapsedTimer>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#else
# include <QDateTime>
#endif
#include "singlecoreapplication.h"
#include "singlecoreapplication_p.h"
SingleCoreApplicationPrivate::SingleCoreApplicationPrivate(SingleCoreApplication *ptr)
: q_ptr(ptr),
memory_(nullptr),
socket_(nullptr),
server_(nullptr),
instanceNumber_(-1) {}
SingleCoreApplicationPrivate::~SingleCoreApplicationPrivate() {
if (socket_ != nullptr) {
socket_->close();
delete socket_;
socket_ = nullptr;
}
if (memory_ != nullptr) {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
if (server_ != nullptr) {
server_->close();
delete server_;
instance->primary = false;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
memory_->unlock();
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
}
void SingleCoreApplicationPrivate::genBlockServerName() {
QCryptographicHash appData(QCryptographicHash::Sha256);
appData.addData("SingleApplication");
appData.addData(SingleCoreApplication::app_t::applicationName().toUtf8());
appData.addData(SingleCoreApplication::app_t::organizationName().toUtf8());
appData.addData(SingleCoreApplication::app_t::organizationDomain().toUtf8());
if (!(options_ & SingleCoreApplication::Mode::ExcludeAppVersion)) {
appData.addData(SingleCoreApplication::app_t::applicationVersion().toUtf8());
}
if (!(options_ & SingleCoreApplication::Mode::ExcludeAppPath)) {
#if defined(Q_OS_UNIX)
const QByteArray appImagePath = qgetenv("APPIMAGE");
if (appImagePath.isEmpty()) {
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toUtf8());
}
else {
appData.addData(appImagePath);
};
#elif defined(Q_OS_WIN)
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toLower().toUtf8());
#else
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toUtf8());
#endif
}
// User level block requires a user specific data in the hash
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("/", "_");
}
void SingleCoreApplicationPrivate::initializeMemoryBlock() const {
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = false;
instance->secondary = 0;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
void SingleCoreApplicationPrivate::startPrimary() {
// Reset the number of connections
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = true;
instance->primaryPid = QCoreApplication::applicationPid();
qstrncpy(instance->primaryUser, getUsername().toUtf8().data(), sizeof(instance->primaryUser));
instance->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();
// 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() {
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->secondary += 1;
instance->checksum = blockChecksum();
instanceNumber_ = instance->secondary;
}
bool SingleCoreApplicationPrivate::connectToPrimary(const int timeout, const 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_->state() == QLocalSocket::ConnectedState) return true;
if (socket_->state() != QLocalSocket::ConnectedState) {
forever {
randomSleep();
if (socket_->state() != QLocalSocket::ConnectingState) {
socket_->connectToServer(blockServerName_);
}
if (socket_->state() == QLocalSocket::ConnectingState) {
socket_->waitForConnected(static_cast<int>(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
QByteArray initMsg;
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
writeStream.setVersion(QDataStream::Qt_5_8);
writeStream << blockServerName_.toLatin1();
writeStream << static_cast<quint8>(connectionType);
writeStream << instanceNumber_;
#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
writeStream << checksum;
return writeConfirmedMessage(static_cast<int>(timeout - time.elapsed()), initMsg);
}
void SingleCoreApplicationPrivate::writeAck(QLocalSocket *sock) {
sock->putChar('\n');
}
bool SingleCoreApplicationPrivate::writeConfirmedMessage(const int timeout, const QByteArray &msg) const {
QElapsedTimer time;
time.start();
// Frame 1: 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>(msg.length());
if (!writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), header)) {
return false;
}
// Frame 2: The message
return writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), msg);
}
bool SingleCoreApplicationPrivate::writeConfirmedFrame(const int timeout, const QByteArray &msg) const {
socket_->write(msg);
socket_->flush();
bool result = socket_->waitForReadyRead(timeout);
if (result) {
socket_->read(1);
return true;
}
return false;
}
quint16 SingleCoreApplicationPrivate::blockChecksum() const {
#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() const {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
qint64 pid = instance->primaryPid;
memory_->unlock();
return pid;
}
QString SingleCoreApplicationPrivate::primaryUser() const {
memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
QByteArray username = instance->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());
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [nextConnSocket, this]() {
const ConnectionInfo &info = connectionMap_[nextConnSocket];
slotClientConnectionClosed(nextConnSocket, info.instanceId);
});
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [nextConnSocket, this]() {
connectionMap_.remove(nextConnSocket);
});
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [nextConnSocket, this]() {
const ConnectionInfo &info = connectionMap_[nextConnSocket];
switch (info.stage) {
case StageInitHeader:
readMessageHeader(nextConnSocket, StageInitBody);
break;
case StageInitBody:
readInitMessageBody(nextConnSocket);
break;
case StageConnectedHeader:
readMessageHeader(nextConnSocket, StageConnectedBody);
break;
case StageConnectedBody:
this->slotDataAvailable(nextConnSocket, info.instanceId);
break;
default:
break;
};
});
}
void SingleCoreApplicationPrivate::readMessageHeader(QLocalSocket *sock, SingleCoreApplicationPrivate::ConnectionStage nextStage) {
if (!connectionMap_.contains(sock)) {
return;
}
if (sock->bytesAvailable() < static_cast<qint64>(sizeof(quint64))) {
return;
}
QDataStream headerStream(sock);
headerStream.setVersion(QDataStream::Qt_5_8);
// Read the header to know the message length
quint64 msgLen = 0;
headerStream >> msgLen;
ConnectionInfo &info = connectionMap_[sock];
info.stage = nextStage;
info.msgLen = msgLen;
writeAck(sock);
}
bool SingleCoreApplicationPrivate::isFrameComplete(QLocalSocket *sock) {
if (!connectionMap_.contains(sock)) {
return false;
}
ConnectionInfo &info = connectionMap_[sock];
return (sock->bytesAvailable() >= static_cast<qint64>(info.msgLen));
}
void SingleCoreApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
Q_Q(SingleCoreApplication);
if (!isFrameComplete(sock)) {
return;
}
// Read the message body
QByteArray msgBytes = sock->readAll();
QDataStream readStream(msgBytes);
readStream.setVersion(QDataStream::Qt_5_8);
// server name
QByteArray latin1Name;
readStream >> latin1Name;
// connection type
ConnectionType connectionType = InvalidConnection;
quint8 connTypeVal = InvalidConnection;
readStream >> connTypeVal;
connectionType = static_cast<ConnectionType>(connTypeVal);
// instance id
quint32 instanceId = 0;
readStream >> instanceId;
// checksum
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;
if (!isValid) {
sock->close();
return;
}
ConnectionInfo &info = connectionMap_[sock];
info.instanceId = instanceId;
info.stage = StageConnectedHeader;
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleCoreApplication::Mode::SecondaryNotification)) {
emit q->instanceStarted();
}
writeAck(sock);
}
void SingleCoreApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
Q_Q(SingleCoreApplication);
if (!isFrameComplete(dataSocket)) {
return;
}
const QByteArray message = dataSocket->readAll();
writeAck(dataSocket);
ConnectionInfo &info = connectionMap_[dataSocket];
info.stage = StageConnectedHeader;
emit q->receivedMessage(instanceId, message);
}
void SingleCoreApplicationPrivate::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
if (closedSocket->bytesAvailable() > 0) {
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(qrand() % 11 + 8);
#endif
}

View File

@@ -1,116 +0,0 @@
// The MIT License (MIT)
//
// 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
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// W A R N I N G !!!
// -----------------
//
// This is a modified version of SingleApplication,
// The original version is at:
//
// https://github.com/itay-grudev/SingleApplication
//
//
#ifndef SINGLECOREAPPLICATION_P_H
#define SINGLECOREAPPLICATION_P_H
#include <QtGlobal>
#include <QObject>
#include <QString>
#include <QHash>
#include "singlecoreapplication.h"
class QLocalServer;
class QLocalSocket;
class QSharedMemory;
struct InstancesInfo {
bool primary;
quint32 secondary;
qint64 primaryPid;
char primaryUser[128];
quint16 checksum;
};
struct ConnectionInfo {
explicit ConnectionInfo() : msgLen(0), instanceId(0), stage(0) {}
quint64 msgLen;
quint32 instanceId;
quint8 stage;
};
class SingleCoreApplicationPrivate : public QObject {
Q_OBJECT
public:
enum ConnectionType : quint8 {
InvalidConnection = 0,
NewInstance = 1,
SecondaryInstance = 2,
Reconnect = 3
};
enum ConnectionStage : quint8 {
StageInitHeader = 0,
StageInitBody = 1,
StageConnectedHeader = 2,
StageConnectedBody = 3,
};
Q_DECLARE_PUBLIC(SingleCoreApplication)
explicit SingleCoreApplicationPrivate(SingleCoreApplication *ptr);
~SingleCoreApplicationPrivate() override;
static QString getUsername();
void genBlockServerName();
void initializeMemoryBlock() const;
void startPrimary();
void startSecondary();
bool connectToPrimary(const int timeout, const ConnectionType connectionType);
quint16 blockChecksum() const;
qint64 primaryPid() const;
QString primaryUser() const;
bool isFrameComplete(QLocalSocket *sock);
void readMessageHeader(QLocalSocket *socket, const ConnectionStage nextStage);
void readInitMessageBody(QLocalSocket *socket);
void writeAck(QLocalSocket *sock);
bool writeConfirmedFrame(const int timeout, const QByteArray &msg) const;
bool writeConfirmedMessage(const int timeout, const QByteArray &msg) const;
static void randomSleep();
SingleCoreApplication *q_ptr;
QSharedMemory *memory_;
QLocalSocket *socket_;
QLocalServer *server_;
quint32 instanceNumber_;
QString blockServerName_;
SingleCoreApplication::Options options_;
QHash<QLocalSocket*, ConnectionInfo> connectionMap_;
public slots:
void slotConnectionEstablished();
void slotDataAvailable(QLocalSocket*, const quint32);
void slotClientConnectionClosed(QLocalSocket*, const quint32);
};
#endif // SINGLECOREAPPLICATION_P_H

View File

@@ -2,8 +2,10 @@ cmake_minimum_required(VERSION 3.7)
project(strawberry)
cmake_policy(SET CMP0054 NEW)
if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.12)
if(POLICY CMP0054)
cmake_policy(SET CMP0054 NEW)
endif()
if(POLICY CMP0074)
cmake_policy(SET CMP0074 NEW)
endif()
@@ -115,9 +117,14 @@ else()
find_package(Iconv)
endif()
find_package(GnuTLS REQUIRED)
find_package(Protobuf REQUIRED)
if(NOT Protobuf_PROTOC_EXECUTABLE)
message(FATAL_ERROR "Missing protobuf compiler.")
if(NOT APPLE)
find_package(Protobuf CONFIG)
endif()
if(NOT Protobuf_FOUND)
find_package(Protobuf REQUIRED)
endif()
if(NOT TARGET protobuf::protoc)
message(FATAL_ERROR "Missing Protobuf compiler.")
endif()
if(LINUX)
find_package(ALSA REQUIRED)
@@ -177,7 +184,7 @@ if(DBUS_FOUND AND NOT WIN32)
list(APPEND QT_COMPONENTS DBus)
endif()
set(QT_OPTIONAL_COMPONENTS Test)
set(QT_MIN_VERSION 5.9)
set(QT_MIN_VERSION 5.12)
if(BUILD_WITH_QT6 OR QT_VERSION_MAJOR EQUAL 6)
set(QT_VERSION_MAJOR 6 CACHE STRING "" FORCE)
@@ -274,6 +281,24 @@ if(X11_FOUND)
else()
message(STATUS "Missing qpa/qplatformnativeinterface.h header.")
endif()
# Check for QX11Application (Qt 6 compiled with XCB).
if(BUILD_WITH_QT6 AND Qt6Gui_VERSION VERSION_GREATER_EQUAL 6.2.0)
set(CMAKE_REQUIRED_FLAGS "-std=c++17")
set(CMAKE_REQUIRED_LIBRARIES ${QtCore_LIBRARIES} ${QtGui_LIBRARIES})
check_cxx_source_compiles("
#include <QGuiApplication>
int main() {
(void)qApp->nativeInterface<QNativeInterface::QX11Application>();
return 0;
}
"
HAVE_QX11APPLICATION
)
unset(CMAKE_REQUIRED_FLAGS)
unset(CMAKE_REQUIRED_LIBRARIES)
endif()
endif(X11_FOUND)
option(USE_TAGLIB "Build with TagLib" OFF)
@@ -308,10 +333,10 @@ if(NOT TAGLIB_FOUND AND NOT TAGPARSER_FOUND)
endif()
# SingleApplication
add_subdirectory(3rdparty/singleapplication)
set(SINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication)
set(SINGLEAPPLICATION_LIBRARIES singleapplication)
set(SINGLECOREAPPLICATION_LIBRARIES singlecoreapplication)
add_subdirectory(3rdparty/kdsingleapplication)
set(SINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/kdsingleapplication)
set(SINGLEAPPLICATION_LIBRARIES kdsingleapplication)
add_definitions(-DKDSINGLEAPPLICATION_STATIC_BUILD)
if(APPLE)
add_subdirectory(3rdparty/SPMediaKeyTap)
@@ -332,10 +357,11 @@ if(WIN32)
endif()
endif()
if(WIN32 AND MSVC)
if(WIN32)
add_subdirectory(3rdparty/getopt)
set(GETOPT_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/getopt)
set(GETOPT_LIBRARIES getopt)
add_definitions(-DSTATIC_GETOPT -D_UNICODE)
endif()
if(WIN32 AND NOT MSVC)
@@ -376,12 +402,12 @@ optional_component(VLC ON "Engine: VLC backend"
optional_component(SONGFINGERPRINTING ON "Song fingerprinting and tracking"
DEPENDS "chromaprint" CHROMAPRINT_FOUND
DEPENDS "gstreamer" GSTREAMER_FOUND
DEPENDS "gstreamer" HAVE_GSTREAMER
)
optional_component(MUSICBRAINZ ON "MusicBrainz integration"
DEPENDS "chromaprint" CHROMAPRINT_FOUND
DEPENDS "gstreamer" GSTREAMER_FOUND
DEPENDS "gstreamer" HAVE_GSTREAMER
)
if(X11_FOUND OR HAVE_DBUS OR APPLE OR WIN32)
@@ -392,21 +418,17 @@ optional_component(GLOBALSHORTCUTS ON "Global shortcuts"
DEPENDS "D-Bus, X11, Windows or macOS" HAVE_GLOBALSHORTCUTS_SUPPORT
)
if(BUILD_WITH_QT6 AND (Qt6Core_VERSION VERSION_EQUAL 6.2.0 OR Qt6Core_VERSION VERSION_GREATER 6.2.0))
optional_component(X11_GLOBALSHORTCUTS ON "X11 global shortcuts" DEPENDS "X11" X11_FOUND)
else()
if(HAVE_X11EXTRAS OR HAVE_QPA_QPLATFORMNATIVEINTERFACE_H)
set(HAVE_X11EXTRAS_OR_QPA_QPLATFORMNATIVEINTERFACE_H ON)
endif()
optional_component(X11_GLOBALSHORTCUTS ON "X11 global shortcuts"
DEPENDS "X11" X11_FOUND
DEPENDS "Qt >= 6.2, X11Extras or qpa/qplatformnativeinterface.h header" HAVE_X11EXTRAS_OR_QPA_QPLATFORMNATIVEINTERFACE_H
)
if(HAVE_QX11APPLICATION OR HAVE_X11EXTRAS OR HAVE_QPA_QPLATFORMNATIVEINTERFACE_H)
set(X11_GLOBALSHORTCUTS_REQUIREMENT_FOUND ON)
endif()
optional_component(X11_GLOBALSHORTCUTS ON "X11 global shortcuts"
DEPENDS "X11" X11_FOUND
DEPENDS "QX11Application, X11Extras or qpa/qplatformnativeinterface.h header" X11_GLOBALSHORTCUTS_REQUIREMENT_FOUND
)
optional_component(AUDIOCD ON "Devices: Audio CD support"
DEPENDS "libcdio" LIBCDIO_FOUND
DEPENDS "gstreamer" GSTREAMER_FOUND
DEPENDS "gstreamer" HAVE_GSTREAMER
)
optional_component(UDISKS2 ON "Devices: UDisks2 backend"
@@ -505,6 +527,8 @@ if(NOT CMAKE_CROSSCOMPILING)
SQLITE_FTS5_TEST
)
endif()
unset(CMAKE_REQUIRED_FLAGS)
unset(CMAKE_REQUIRED_LIBRARIES)
endif()
# Set up definitions

112
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,112 @@
# Contribution guidelines
Strawberry is an free and open-source project, it is possible and encouraged to participate in the development.
You can also participate by answering questions, reporting bugs or helping with documentation.
## Submitting a pull request
You should start by creating a fork of the Strawberry repository using the GitHub
fork button, after that you can clone the repository from your fork.
Replace "username" with your own.
### Clone the repository
git clone git@github.com:username/strawberry.git
cd strawberry
### Setup the remote
git remote add upstream git@github.com:strawberrymusicplayer/strawberry.git
### Create a new branch
This creates a new branch from the master branch that you use for specific
changes.
git checkout -b your-branch
### Stage changes
Once you've finished working on a specific change, stage the changes for
a specific commit.
Always keep your commits relevant to the pull request, and each commit as
small as possible.
git add -p
### Commit changes
git commit
### Commit messages
The first line should start with "Class:", which referer to the class
that is changed. Don't use a trailing period after the first line.
If this change affects more than one class, omit the class and write a
more general message.
You only need to include a main description (body) for larger changes
where the one line is not enough to describe everything.
The main description starts after two newlines, it is normal prose and
should use normal punctuation and capital letters where appropriate.
An example of the expected format for git commit messages is as follows:
```
class: Short explanation of the commit
Longer explanation explaining exactly what's changed, why it's changed,
and what bugs were fixed.
Fixes #1234
```
### Push the changes to GitHub
Once you've finished working on the changes, push the branch
to the Git repository and open a new pull request.
git push origin your-branch
### Update your fork's master branch
git checkout master
git pull --rebase origin master
git fetch upstream
git merge upstream/master
git push origin master
### Update your branch
git checkout your-branch
git fetch upstream
git rebase upstream/master
git push origin your-branch --force-with-lease
### Rebase your branch
If you need fix any issues with your commits, you need to rebase your
branch to squash any commits, or to change the commit message.
git checkout your-branch
git log
git rebase -i commit_sha~
git push origin your-branch --force-with-lease
### Delete your fork
If you do not plan to work more on Strawberry, please delete your fork from GitHub
once the pull requests are merged.

View File

@@ -2,6 +2,99 @@ Strawberry Music Player
=======================
ChangeLog
Version 1.0.18 (2023.07.02):
Bugfixes:
* Fixed reading disc from QObuz songs (#1168).
* Fixed volume being reset on playback with PulseAudio (#1174).
* Fixed <br> tags in SQL query error message.
* Fixed compile with Qt 6 without XCB (QX11Application).
* Fixed smart playlist editor not properly loading search terms (#1172).
* Fixed use of fixed icon for playlist favorite star icon (#1178).
* Possible fix for collection thumbnails using disk cache having identical covers for albums with hashtag (#) in the album title (#1183).
* Fixed listenbrainz scrobbling for songs with multiple artist mbids.
* Fixed listenbrainz scrobbling for songs without duration.
* Fixed gapless playback sometimes not working.
* Fixed writing PNG images as embedded covers (#1209).
* Fixed greyscale album covers not working in OSD D-Bus (#1205).
* Fixed collection thumbnail disk cache with Qt 6.5.1 and newer.
* Fixed moodbar disk cache with Qt 6.5.1 and newer.
* Fixed playlist edit tag F2 shortcut only working for title tag (#1210).
* Append number to filename if the destination file already exist when transcoding audio (#1200).
* Fixed abseil linking issues with protobuf 1.22.0 and newer.
* (macOS) Fixed "Show this message" checkbox having no affect on Rosetta warning dialog (#1180).
* (macOS) Disable unused D-Bus.
* (Windows) Fixed command line options not working with diacritics (#1191).
* (Windows) Fixed issue with saving album covers in album directory being saved in temp directory instead.
* (Windows) Fixed crash when trying a play a song which doesn't exist, gstreamer issue #1683 (#1214).
Enhancements:
* Reduce memory overhead with album cover handling (#1046).
* Improved listenbrainz error handling.
* Show error dialog for listenbrainz errors similar to last.fm/libre.fm.
* Reduce NetworkAccessManager instances.
* Replace SingleApplication with KDSingleApplication.
* Require Qt 5.12 or higher.
* Add new database fields for art_embedded and art_unset.
* Rewrite album cover loader.
* Move cover filename settings from collection to covers settings.
* Add setting to set priorities for album cover types.
* Add rating filtering to playlist search (#1212).
* (Windows|MSVC) Add WSAPI2 plugin.
Version 1.0.17 (2023.03.29):
Bugfixes:
* Fixed over-sized context album cover with device pixel ratio higher than 1.0 (#1166).
* Fixed playing widget fading from a blurry previous cover with device pixel ratio higher than 1.0.
* Made playlist source icon, album cover manager and OSD pretty cover respect device pixel ratio.
Version 1.0.16 (2023.03.27):
Bugfixes:
* Fixed lyrics from Musixmatch.
* Fixed possible file corruption when saving both tags and embedded cover using the tag editor (#1158).
* Fixed compile without GStreamer.
* Fixed context and playing now album art rendering on High DPI displays (#1161).
* Fixed setting source properties (device, user-agent, ssl-strict) with GStreamer 1.22 (playbin3) and higher (#1148).
* Fixed rescan songs feature not ignoring mtime.
* Search lyrics by artist instead of album artist by default.
Code improvements:
* Replace use of deprecated QSqlDatabase::exec().
Added features:
* Added backend setting for strict SSL mode.
* Read AcoustID and MusicBrainz tags.
* Submit MusicBrainz tags with ListenBrainz.
Version 1.0.15 (2023.03.04):
Bugfixes:
* Fixed playlist column showing invalid last played date for streams.
* Fixed crash when the audio bin failed to initialize (#1123, #1133).
* Fixed duplicated filename when organizing files using dot in the filename (#1136).
* Fixed tag inline editing for streams (#1130).
* Fixed resetting play statistics using tag edit dialog (#1124).
* Fixed compilation songs not showing if group by was set to other than (Album) Artist / Album (#1140).
Enhancements:
* Added lyrics from stands4 (lyrics.com).
* Added Sonogram analyzer.
* Use GStreamer playbin3 with GStreamer 1.22.0 and higher.
Code improvements:
* Made use of C++11 enum class where possible.
* Use new QNativeIpcKey based QSharedMemory constructor with Qt 6.6 and higher.
Version 1.0.14 (2023.01.13):
Bugfixes:
* Fix initial volume not set when using Auto as output (#1104).
* Fix saving moodbar if the URL contains host, ie.: UNC paths for SMB (#1101).
* Fix CollectionBackendTest compile error (#1100).
* Remove explicitly enabling debug messages (#1106).
Version 1.0.13 (2023.01.09):
Bugfixes:

View File

@@ -51,7 +51,7 @@ Funding developers is a way to contribute to open source projects you appreciate
* Edit tags on audio files
* Fetch tags from MusicBrainz
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
* Song lyrics from [AudD](https://audd.io/), [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
* Song lyrics from [Lyrics.com](https://www.lyrics.com/), [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/)
* Support for multiple backends
* Audio analyzer
* Audio equalizer
@@ -73,7 +73,7 @@ To build Strawberry from source you need the following installed on your system
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
* [Boost](https://www.boost.org/)
* [GLib](https://developer.gnome.org/glib/)
* [Qt 6 or Qt 5.9 or higher with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/)
* [Qt 6 or Qt 5.12 or higher with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/)
* [SQLite 3.9 or newer with FTS5](https://www.sqlite.org)
* [Protobuf](https://developers.google.com/protocol-buffers/)
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
@@ -118,4 +118,3 @@ To compile on Windows with Visual Studio 2019 or 2022, see https://github.com/st
### :penguin: Packaging status
[![Packaging status](https://repology.org/badge/vertical-allrepos/strawberry.svg?exclude_unsupported=1)](https://repology.org/metapackage/strawberry/versions)

View File

@@ -1,6 +1,6 @@
set(STRAWBERRY_VERSION_MAJOR 1)
set(STRAWBERRY_VERSION_MINOR 0)
set(STRAWBERRY_VERSION_PATCH 13)
set(STRAWBERRY_VERSION_PATCH 18)
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
set(INCLUDE_GIT_REVISION OFF)

View File

@@ -7,6 +7,8 @@
<file>schema/schema-13.sql</file>
<file>schema/schema-14.sql</file>
<file>schema/schema-15.sql</file>
<file>schema/schema-16.sql</file>
<file>schema/schema-17.sql</file>
<file>schema/device-schema.sql</file>
<file>style/strawberry.css</file>
<file>style/smartplaylistsearchterm.css</file>

View File

@@ -59,15 +59,31 @@ CREATE TABLE device_%deviceid_songs (
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_embedded INTEGER DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
art_unset INTEGER DEFAULT 0,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -80,4 +96,4 @@ CREATE VIRTUAL TABLE device_%deviceid_fts USING fts5(
tokenize = "unicode61 remove_diacritics 1"
);
UPDATE devices SET schema_version=3 WHERE ROWID=%deviceid;
UPDATE devices SET schema_version=4 WHERE ROWID=%deviceid;

217
data/schema/schema-16.sql Normal file
View File

@@ -0,0 +1,217 @@
ALTER TABLE songs ADD COLUMN acoustid_id TEXT;
ALTER TABLE songs ADD COLUMN acoustid_fingerprint TEXT;
ALTER TABLE songs ADD COLUMN musicbrainz_album_artist_id TEXT;
ALTER TABLE songs ADD COLUMN musicbrainz_artist_id TEXT;
ALTER TABLE songs ADD COLUMN musicbrainz_original_artist_id TEXT;
ALTER TABLE songs ADD COLUMN musicbrainz_album_id TEXT;
ALTER TABLE songs ADD COLUMN musicbrainz_original_album_id TEXT;
ALTER TABLE songs ADD COLUMN musicbrainz_recording_id TEXT;
ALTER TABLE songs ADD COLUMN musicbrainz_track_id TEXT;
ALTER TABLE songs ADD COLUMN musicbrainz_disc_id TEXT;
ALTER TABLE songs ADD COLUMN musicbrainz_release_group_id TEXT;
ALTER TABLE songs ADD COLUMN musicbrainz_work_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN acoustid_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN acoustid_fingerprint TEXT;
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_artist_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_album_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_original_album_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_recording_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_track_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_disc_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_release_group_id TEXT;
ALTER TABLE subsonic_songs ADD COLUMN musicbrainz_work_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN acoustid_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN acoustid_fingerprint TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_artist_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_album_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_original_album_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_recording_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_track_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_disc_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_release_group_id TEXT;
ALTER TABLE tidal_artists_songs ADD COLUMN musicbrainz_work_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN acoustid_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN acoustid_fingerprint TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_artist_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_album_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_original_album_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_recording_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_track_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_disc_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_release_group_id TEXT;
ALTER TABLE tidal_albums_songs ADD COLUMN musicbrainz_work_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN acoustid_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN acoustid_fingerprint TEXT;
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_artist_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_album_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_original_album_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_recording_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_track_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_disc_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_release_group_id TEXT;
ALTER TABLE tidal_songs ADD COLUMN musicbrainz_work_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN acoustid_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN acoustid_fingerprint TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_artist_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_album_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_original_album_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_recording_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_track_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_disc_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_release_group_id TEXT;
ALTER TABLE qobuz_artists_songs ADD COLUMN musicbrainz_work_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN acoustid_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN acoustid_fingerprint TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_artist_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_album_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_original_album_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_recording_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_track_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_disc_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_release_group_id TEXT;
ALTER TABLE qobuz_albums_songs ADD COLUMN musicbrainz_work_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN acoustid_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN acoustid_fingerprint TEXT;
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_album_artist_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_artist_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_original_artist_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_album_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_original_album_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_recording_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_track_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_disc_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_release_group_id TEXT;
ALTER TABLE qobuz_songs ADD COLUMN musicbrainz_work_id TEXT;
ALTER TABLE playlist_items ADD COLUMN acoustid_id TEXT;
ALTER TABLE playlist_items ADD COLUMN acoustid_fingerprint TEXT;
ALTER TABLE playlist_items ADD COLUMN musicbrainz_album_artist_id TEXT;
ALTER TABLE playlist_items ADD COLUMN musicbrainz_artist_id TEXT;
ALTER TABLE playlist_items ADD COLUMN musicbrainz_original_artist_id TEXT;
ALTER TABLE playlist_items ADD COLUMN musicbrainz_album_id TEXT;
ALTER TABLE playlist_items ADD COLUMN musicbrainz_original_album_id TEXT;
ALTER TABLE playlist_items ADD COLUMN musicbrainz_recording_id TEXT;
ALTER TABLE playlist_items ADD COLUMN musicbrainz_track_id TEXT;
ALTER TABLE playlist_items ADD COLUMN musicbrainz_disc_id TEXT;
ALTER TABLE playlist_items ADD COLUMN musicbrainz_release_group_id TEXT;
ALTER TABLE playlist_items ADD COLUMN musicbrainz_work_id TEXT;
UPDATE schema_version SET version=16;

45
data/schema/schema-17.sql Normal file
View File

@@ -0,0 +1,45 @@
ALTER TABLE songs ADD COLUMN art_embedded INTEGER DEFAULT 0;
ALTER TABLE songs ADD COLUMN art_unset INTEGER DEFAULT 0;
ALTER TABLE subsonic_songs ADD COLUMN art_embedded INTEGER DEFAULT 0;
ALTER TABLE subsonic_songs ADD COLUMN art_unset INTEGER DEFAULT 0;
ALTER TABLE tidal_artists_songs ADD COLUMN art_embedded INTEGER DEFAULT 0;
ALTER TABLE tidal_artists_songs ADD COLUMN art_unset INTEGER DEFAULT 0;
ALTER TABLE tidal_albums_songs ADD COLUMN art_embedded INTEGER DEFAULT 0;
ALTER TABLE tidal_albums_songs ADD COLUMN art_unset INTEGER DEFAULT 0;
ALTER TABLE tidal_songs ADD COLUMN art_embedded INTEGER DEFAULT 0;
ALTER TABLE tidal_songs ADD COLUMN art_unset INTEGER DEFAULT 0;
ALTER TABLE qobuz_artists_songs ADD COLUMN art_embedded INTEGER DEFAULT 0;
ALTER TABLE qobuz_artists_songs ADD COLUMN art_unset INTEGER DEFAULT 0;
ALTER TABLE qobuz_albums_songs ADD COLUMN art_embedded INTEGER DEFAULT 0;
ALTER TABLE qobuz_albums_songs ADD COLUMN art_unset INTEGER DEFAULT 0;
ALTER TABLE qobuz_songs ADD COLUMN art_embedded INTEGER DEFAULT 0;
ALTER TABLE qobuz_songs ADD COLUMN art_unset INTEGER DEFAULT 0;
ALTER TABLE playlist_items ADD COLUMN art_embedded INTEGER DEFAULT 0;
ALTER TABLE playlist_items ADD COLUMN art_unset INTEGER DEFAULT 0;
UPDATE songs SET art_embedded = 1 WHERE art_automatic = 'file:(embedded)';
UPDATE songs SET art_automatic = '' WHERE art_automatic = 'file:(embedded)';
UPDATE songs SET art_unset = 1 WHERE art_manual = 'file:(unset)';
UPDATE songs SET art_manual = '' WHERE art_manual = 'file:(unset)';
UPDATE schema_version SET version=17;

View File

@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
DELETE FROM schema_version;
INSERT INTO schema_version (version) VALUES (15);
INSERT INTO schema_version (version) VALUES (17);
CREATE TABLE IF NOT EXISTS directories (
path TEXT NOT NULL,
@@ -67,15 +67,31 @@ CREATE TABLE IF NOT EXISTS songs (
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_embedded INTEGER DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
art_unset INTEGER DEFAULT 0,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -129,15 +145,31 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_embedded INTEGER DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
art_unset INTEGER DEFAULT 0,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -191,15 +223,31 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_embedded INTEGER DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
art_unset INTEGER DEFAULT 0,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -253,15 +301,31 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_embedded INTEGER DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
art_unset INTEGER DEFAULT 0,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -315,15 +379,31 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_embedded INTEGER DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
art_unset INTEGER DEFAULT 0,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -377,15 +457,31 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_embedded INTEGER DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
art_unset INTEGER DEFAULT 0,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -439,15 +535,31 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_embedded INTEGER DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
art_unset INTEGER DEFAULT 0,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -501,15 +613,31 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
compilation_off INTEGER NOT NULL DEFAULT 0,
compilation_effective INTEGER NOT NULL DEFAULT 0,
art_embedded INTEGER DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
art_unset INTEGER DEFAULT 0,
effective_albumartist TEXT,
effective_originalyear INTEGER NOT NULL DEFAULT 0,
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);
@@ -583,15 +711,31 @@ CREATE TABLE IF NOT EXISTS playlist_items (
compilation_off INTEGER DEFAULT 0,
compilation_effective INTEGER DEFAULT 0,
art_embedded INTEGER DEFAULT 0,
art_automatic TEXT,
art_manual TEXT,
art_unset INTEGER DEFAULT 0,
effective_albumartist TEXT,
effective_originalyear INTEGER,
cue_path TEXT,
rating INTEGER DEFAULT -1
rating INTEGER DEFAULT -1,
acoustid_id TEXT,
acoustid_fingerprint TEXT,
musicbrainz_album_artist_id TEXT,
musicbrainz_artist_id TEXT,
musicbrainz_original_artist_id TEXT,
musicbrainz_album_id TEXT,
musicbrainz_original_album_id TEXT,
musicbrainz_recording_id TEXT,
musicbrainz_track_id TEXT,
musicbrainz_disc_id TEXT,
musicbrainz_release_group_id TEXT,
musicbrainz_work_id TEXT
);

View File

@@ -64,11 +64,3 @@ QToolButton::menu-button {
width: 16px;
border: none;
}
macos {
font-size: 11pt;
}
macos QMenu {
font-size: 13pt;
}

3
debian/control.in vendored
View File

@@ -3,6 +3,7 @@ Section: sound
Priority: optional
Maintainer: Jonas Kvinge <jonas@jkvinge.net>
Build-Depends: debhelper (>= 11),
git,
make,
cmake,
gcc,
@@ -52,7 +53,7 @@ Description: music player and music collection organizer
- Edit tags on audio files
- Automatically retrieve tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
- Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
- Audio analyzer
- Audio equalizer
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic

10
debian/copyright vendored
View File

@@ -25,8 +25,6 @@ Files: src/core/main.h
src/context/contextview.h
src/context/contextalbum.cpp
src/context/contextalbum.h
src/engine/enginetype.cpp
src/engine/enginetype.h
src/engine/alsadevicefinder.cpp
src/engine/alsadevicefinder.h
src/engine/alsapcmdevicefinder.cpp
@@ -37,6 +35,8 @@ Files: src/core/main.h
src/engine/devicefinder.h
src/engine/enginedevice.cpp
src/engine/enginedevice.h
src/engine/enginemetadata.cpp
src/engine/enginemetadata.h
src/internet/internetservice.cpp
src/internet/internetservice.h
src/internet/internettabsview.cpp
@@ -267,8 +267,8 @@ Copyright: 2010, Spotify AB
2011, Joachim Bengtsson
License: BSD-3-clause
Files: 3rdparty/singleapplication/*
Copyright: 2015-2022, Itay Grudev
Files: 3rdparty/kdsingleapplication/*
Copyright: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
License: MIT
@@ -376,7 +376,7 @@ License: Apache-2.0
On Debian systems, the complete text of the Apache 2.0 license can be
found in the file
`/usr/share/common-licenses/Apache-2.0`.
License: BSD-3-clause
Copyright (c) The Regents of the University of California.
All rights reserved.

View File

@@ -33,7 +33,7 @@
<key>LSApplicationCategoryType</key>
<string>public.app-category.music</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.4</string>
<string>11.0</string>
<key>SUFeedURL</key>
<string>https://www.strawberrymusicplayer.org/sparkle-macos</string>
<key>SUPublicEDKey</key>

View File

@@ -29,7 +29,7 @@
<li>Edit tags on audio files</li>
<li>Automatically retrieve tags from MusicBrainz</li>
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
<li>Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com</li>
<li>Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com</li>
<li>Support for multiple backends</li>
<li>Audio analyzer and equalizer</li>
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
@@ -50,6 +50,6 @@
</screenshots>
<update_contact>eclipseo@fedoraproject.org</update_contact>
<releases>
<release version="1.0.0" date="2021-10-16"/>
<release version="1.0.18" date="2023-07-02"/>
</releases>
</component>

View File

@@ -3,7 +3,9 @@ Version=1.0
Type=Application
Name=Strawberry
GenericName=Strawberry Music Player
GenericName[ru]=Музыкальный проигрыватель Strawberry
Comment=Plays music
Comment[ru]=Прослушивание музыки
Exec=strawberry %U
TryExec=strawberry
Icon=strawberry
@@ -18,19 +20,24 @@ Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
[Desktop Action Play-Pause]
Name=Play/Pause
Exec=strawberry --play-pause
Name[ru]=Играть/пауза
[Desktop Action Stop]
Name=Stop
Exec=strawberry --stop
Name[ru]=Стоп
[Desktop Action StopAfterCurrent]
Name=Stop after this track
Exec=strawberry --stop-after-current
Name[ru]=Стоп после этого трека
[Desktop Action Previous]
Name=Previous
Exec=strawberry --previous
Name[ru]=Предыдущий
[Desktop Action Next]
Name=Next
Exec=strawberry --next
Name[ru]=Следующий

View File

@@ -29,7 +29,7 @@ Features:
.br
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
.br
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
- Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
.br
- Support for multiple backends
.br

View File

@@ -119,7 +119,7 @@ Features:
- Edit tags on audio files
- Automatically retrieve tags from MusicBrainz
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
- Song lyrics from AudD, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
- Song lyrics from Lyrics.com, Genius, Musixmatch, ChartLyrics, lyrics.ovh and lololyrics.com
- Support for multiple backends
- Audio analyzer
- Audio equalizer

View File

@@ -1,57 +1,68 @@
!define build_type ""
!define compiler "unknown"
!define arch "unknown"
!if "@ARCH@" == "x86"
!define arch_x86
!undef arch
!define arch "x86"
!endif
!if "@ARCH@" == "i686-w64-mingw32.shared"
!define arch_x86
!undef arch
!define arch "x86"
!endif
!if "@ARCH@" == "x86_64"
!define arch_x64
!undef arch
!define arch "x64"
!endif
!if "@ARCH@" == "x86_64-w64-mingw32.shared"
!define arch_x64
!undef arch
!define arch "x64"
!endif
!if "@MINGW@" == "1"
!define mingw
!undef compiler
!define compiler "mingw"
!endif
!if "@MSVC@" == "1"
!define msvc
!undef compiler
!define compiler "msvc"
!endif
!if "@ARCH@" == "x86"
!define arch_x86
!else if "@ARCH@" == "i686"
!define arch_x86
!else if "@ARCH@" == "i686-w64-mingw32.shared"
!define arch_x86
!else if "@ARCH@" == "x64"
!define arch_x64
!else if "@ARCH@" == "x86_64"
!define arch_x64
!else if "@ARCH@" == "x86_64-w64-mingw32.shared"
!define arch_x64
!endif
!ifdef arch_x86
!define arch "x86"
!endif
!ifdef arch_x64
!define arch "x64"
!endif
!if "@CMAKE_BUILD_TYPE@" == "Release"
!define release
!endif
!if "@CMAKE_BUILD_TYPE@" == "RelWithDebInfo"
!else if "@CMAKE_BUILD_TYPE@" == "RelWithDebInfo"
!define release
!else if "@CMAKE_BUILD_TYPE@" == "Debug"
!define debug
!endif
!if "@CMAKE_BUILD_TYPE@" == "Debug"
!define debug
!undef build_type
!ifdef release
!define build_type ""
!endif
!ifdef debug
!define build_type "-Debug"
!endif
!ifndef compiler
!error "Missing compiler."
!endif
!ifndef build_type
!error "Missing build type."
!endif
!ifndef arch
!error "Missing arch."
!endif
!ifdef debug
!define PRODUCT_NAME "Strawberry Music Player Debug"
!define PRODUCT_NAME_SHORT "Strawberry"
@@ -241,10 +252,10 @@ Section "Strawberry" Strawberry
File "libssl-3-x64.dll"
!endif
File "avcodec-59.dll"
File "avfilter-8.dll"
File "avformat-59.dll"
File "avutil-57.dll"
File "avcodec-60.dll"
File "avfilter-9.dll"
File "avformat-60.dll"
File "avutil-58.dll"
File "libFLAC-12.dll"
File "libbrotlicommon.dll"
File "libbrotlidec.dll"
@@ -301,7 +312,6 @@ Section "Strawberry" Strawberry
File "libopus-0.dll"
File "liborc-0.4-0.dll"
File "libpng16-16.dll"
File "libprotobuf-32.dll"
File "libpsl-5.dll"
File "libqtsparkle-qt6.dll"
File "libsoup-3.0-0.dll"
@@ -320,11 +330,51 @@ Section "Strawberry" Strawberry
File "libwinpthread-1.dll"
File "libxml2-2.dll"
File "libzstd.dll"
File "postproc-56.dll"
File "postproc-57.dll"
File "swresample-4.dll"
File "swscale-6.dll"
File "swscale-7.dll"
File "zlib1.dll"
File "libabsl_base.dll"
File "libabsl_city.dll"
File "libabsl_cord.dll"
File "libabsl_cord_internal.dll"
File "libabsl_cordz_handle.dll"
File "libabsl_cordz_info.dll"
File "libabsl_crc32c.dll"
File "libabsl_crc_cord_state.dll"
File "libabsl_crc_internal.dll"
File "libabsl_die_if_null.dll"
File "libabsl_examine_stack.dll"
File "libabsl_hash.dll"
File "libabsl_int128.dll"
File "libabsl_log_globals.dll"
File "libabsl_log_internal_check_op.dll"
File "libabsl_log_internal_format.dll"
File "libabsl_log_internal_globals.dll"
File "libabsl_log_internal_log_sink_set.dll"
File "libabsl_log_internal_message.dll"
File "libabsl_log_internal_nullguard.dll"
File "libabsl_log_internal_proto.dll"
File "libabsl_log_sink.dll"
File "libabsl_low_level_hash.dll"
File "libabsl_malloc_internal.dll"
File "libabsl_raw_hash_set.dll"
File "libabsl_raw_logging_internal.dll"
File "libabsl_spinlock_wait.dll"
File "libabsl_stacktrace.dll"
File "libabsl_status.dll"
File "libabsl_statusor.dll"
File "libabsl_strerror.dll"
File "libabsl_str_format_internal.dll"
File "libabsl_strings.dll"
File "libabsl_strings_internal.dll"
File "libabsl_symbolize.dll"
File "libabsl_synchronization.dll"
File "libabsl_throw_delegate.dll"
File "libabsl_time.dll"
File "libabsl_time_zone.dll"
!ifdef debug
File "gdb.exe"
File "libexpat-1.dll"
@@ -334,6 +384,7 @@ Section "Strawberry" Strawberry
File "libpcre2-16d.dll"
File "libreadline8.dll"
File "libtermcap.dll"
File "libabsl_graphcycles_internal.dll"
!else
File "libpcre2-8.dll"
File "libpcre2-16.dll"
@@ -388,12 +439,12 @@ Section "Strawberry" Strawberry
File "gsttag-1.0-0.dll"
File "gsturidownloader-1.0-0.dll"
File "gstvideo-1.0-0.dll"
File "gstwinrt-1.0-0.dll"
File "harfbuzz.dll"
File "intl-8.dll"
File "jpeg62.dll"
File "libbs2b.dll"
File "libfaac_dll.dll"
File "libiconv.dll"
File "liblzma.dll"
File "libmp3lame.dll"
File "libopenmpt.dll"
@@ -415,11 +466,12 @@ Section "Strawberry" Strawberry
File "vorbis.dll"
File "vorbisfile.dll"
File "wavpackdll.dll"
File "abseil_dll.dll"
!ifdef release
File "freetype.dll"
File "libiconv.dll"
File "libpng16.dll"
File "libprotobuf.dll"
File "libxml2.dll"
File "pcre2-8.dll"
File "pcre2-16.dll"
@@ -428,8 +480,8 @@ Section "Strawberry" Strawberry
!endif
!ifdef debug
File "freetyped.dll"
File "libiconvd.dll"
File "libpng16d.dll"
File "libprotobufd.dll"
File "libxml2d.dll"
File "pcre2-8d.dll"
File "pcre2-16d.dll"
@@ -437,15 +489,26 @@ Section "Strawberry" Strawberry
File "zlibd.dll"
!endif
; Used by libfftw3-3.dll because fftw is compiled with MinGW.
!ifdef arch_x86
File "libgcc_s_sjlj-1.dll"
File "libwinpthread-1.dll"
!endif
!endif ; MSVC
; Common files
File "icudt72.dll"
File "icudt73.dll"
File "libfftw3-3.dll"
!ifdef debug
File "libprotobufd.dll"
!else
File "libprotobuf.dll"
!endif
!ifdef msvc && debug
File "icuin72d.dll"
File "icuuc72d.dll"
File "icuin73d.dll"
File "icuuc73d.dll"
File "Qt6Concurrentd.dll"
File "Qt6Cored.dll"
File "Qt6Guid.dll"
@@ -453,8 +516,8 @@ Section "Strawberry" Strawberry
File "Qt6Sqld.dll"
File "Qt6Widgetsd.dll"
!else
File "icuin72.dll"
File "icuuc72.dll"
File "icuin73.dll"
File "icuuc73.dll"
File "Qt6Concurrent.dll"
File "Qt6Core.dll"
File "Qt6Gui.dll"
@@ -677,6 +740,7 @@ Section "Gstreamer plugins" gstreamer-plugins
File "/oname=gstvolume.dll" "gstreamer-plugins\gstvolume.dll"
File "/oname=gstvorbis.dll" "gstreamer-plugins\gstvorbis.dll"
File "/oname=gstwasapi.dll" "gstreamer-plugins\gstwasapi.dll"
File "/oname=gstwasapi2.dll" "gstreamer-plugins\gstwasapi2.dll"
File "/oname=gstwavenc.dll" "gstreamer-plugins\gstwavenc.dll"
File "/oname=gstwavpack.dll" "gstreamer-plugins\gstwavpack.dll"
File "/oname=gstwavparse.dll" "gstreamer-plugins\gstwavparse.dll"
@@ -743,10 +807,10 @@ Section "Uninstall"
Delete "$INSTDIR\libssl-3-x64.dll"
!endif
Delete "$INSTDIR\avcodec-59.dll"
Delete "$INSTDIR\avfilter-8.dll"
Delete "$INSTDIR\avformat-59.dll"
Delete "$INSTDIR\avutil-57.dll"
Delete "$INSTDIR\avcodec-60.dll"
Delete "$INSTDIR\avfilter-9.dll"
Delete "$INSTDIR\avformat-60.dll"
Delete "$INSTDIR\avutil-58.dll"
Delete "$INSTDIR\libFLAC-12.dll"
Delete "$INSTDIR\libbrotlicommon.dll"
Delete "$INSTDIR\libbrotlidec.dll"
@@ -803,7 +867,6 @@ Section "Uninstall"
Delete "$INSTDIR\libopus-0.dll"
Delete "$INSTDIR\liborc-0.4-0.dll"
Delete "$INSTDIR\libpng16-16.dll"
Delete "$INSTDIR\libprotobuf-32.dll"
Delete "$INSTDIR\libpsl-5.dll"
Delete "$INSTDIR\libqtsparkle-qt6.dll"
Delete "$INSTDIR\libsoup-3.0-0.dll"
@@ -822,11 +885,51 @@ Section "Uninstall"
Delete "$INSTDIR\libwinpthread-1.dll"
Delete "$INSTDIR\libxml2-2.dll"
Delete "$INSTDIR\libzstd.dll"
Delete "$INSTDIR\postproc-56.dll"
Delete "$INSTDIR\postproc-57.dll"
Delete "$INSTDIR\swresample-4.dll"
Delete "$INSTDIR\swscale-6.dll"
Delete "$INSTDIR\swscale-7.dll"
Delete "$INSTDIR\zlib1.dll"
Delete "$INSTDIR\libabsl_base.dll"
Delete "$INSTDIR\libabsl_city.dll"
Delete "$INSTDIR\libabsl_cord.dll"
Delete "$INSTDIR\libabsl_cord_internal.dll"
Delete "$INSTDIR\libabsl_cordz_handle.dll"
Delete "$INSTDIR\libabsl_cordz_info.dll"
Delete "$INSTDIR\libabsl_crc32c.dll"
Delete "$INSTDIR\libabsl_crc_cord_state.dll"
Delete "$INSTDIR\libabsl_crc_internal.dll"
Delete "$INSTDIR\libabsl_die_if_null.dll"
Delete "$INSTDIR\libabsl_examine_stack.dll"
Delete "$INSTDIR\libabsl_hash.dll"
Delete "$INSTDIR\libabsl_int128.dll"
Delete "$INSTDIR\libabsl_log_globals.dll"
Delete "$INSTDIR\libabsl_log_internal_check_op.dll"
Delete "$INSTDIR\libabsl_log_internal_format.dll"
Delete "$INSTDIR\libabsl_log_internal_globals.dll"
Delete "$INSTDIR\libabsl_log_internal_log_sink_set.dll"
Delete "$INSTDIR\libabsl_log_internal_message.dll"
Delete "$INSTDIR\libabsl_log_internal_nullguard.dll"
Delete "$INSTDIR\libabsl_log_internal_proto.dll"
Delete "$INSTDIR\libabsl_log_sink.dll"
Delete "$INSTDIR\libabsl_low_level_hash.dll"
Delete "$INSTDIR\libabsl_malloc_internal.dll"
Delete "$INSTDIR\libabsl_raw_hash_set.dll"
Delete "$INSTDIR\libabsl_raw_logging_internal.dll"
Delete "$INSTDIR\libabsl_spinlock_wait.dll"
Delete "$INSTDIR\libabsl_stacktrace.dll"
Delete "$INSTDIR\libabsl_status.dll"
Delete "$INSTDIR\libabsl_statusor.dll"
Delete "$INSTDIR\libabsl_strerror.dll"
Delete "$INSTDIR\libabsl_str_format_internal.dll"
Delete "$INSTDIR\libabsl_strings.dll"
Delete "$INSTDIR\libabsl_strings_internal.dll"
Delete "$INSTDIR\libabsl_symbolize.dll"
Delete "$INSTDIR\libabsl_synchronization.dll"
Delete "$INSTDIR\libabsl_throw_delegate.dll"
Delete "$INSTDIR\libabsl_time.dll"
Delete "$INSTDIR\libabsl_time_zone.dll"
!ifdef debug
Delete "$INSTDIR\gdb.exe"
Delete "$INSTDIR\libexpat-1.dll"
@@ -836,6 +939,7 @@ Section "Uninstall"
Delete "$INSTDIR\libpcre2-16d.dll"
Delete "$INSTDIR\libreadline8.dll"
Delete "$INSTDIR\libtermcap.dll"
Delete "$INSTDIR\libabsl_graphcycles_internal.dll"
!else
Delete "$INSTDIR\libpcre2-8.dll"
Delete "$INSTDIR\libpcre2-16.dll"
@@ -890,12 +994,12 @@ Section "Uninstall"
Delete "$INSTDIR\gsttag-1.0-0.dll"
Delete "$INSTDIR\gsturidownloader-1.0-0.dll"
Delete "$INSTDIR\gstvideo-1.0-0.dll"
Delete "$INSTDIR\gstwinrt-1.0-0.dll"
Delete "$INSTDIR\harfbuzz.dll"
Delete "$INSTDIR\intl-8.dll"
Delete "$INSTDIR\jpeg62.dll"
Delete "$INSTDIR\libbs2b.dll"
Delete "$INSTDIR\libfaac_dll.dll"
Delete "$INSTDIR\libiconv.dll"
Delete "$INSTDIR\liblzma.dll"
Delete "$INSTDIR\libmp3lame.dll"
Delete "$INSTDIR\libopenmpt.dll"
@@ -917,11 +1021,12 @@ Section "Uninstall"
Delete "$INSTDIR\vorbis.dll"
Delete "$INSTDIR\vorbisfile.dll"
Delete "$INSTDIR\wavpackdll.dll"
Delete "$INSTDIR\abseil_dll.dll"
!ifdef release
Delete "$INSTDIR\freetype.dll"
Delete "$INSTDIR\libiconv.dll"
Delete "$INSTDIR\libpng16.dll"
Delete "$INSTDIR\libprotobuf.dll"
Delete "$INSTDIR\libxml2.dll"
Delete "$INSTDIR\pcre2-8.dll"
Delete "$INSTDIR\pcre2-16.dll"
@@ -930,8 +1035,8 @@ Section "Uninstall"
!endif
!ifdef debug
Delete "$INSTDIR\freetyped.dll"
Delete "$INSTDIR\libiconvd.dll"
Delete "$INSTDIR\libpng16d.dll"
Delete "$INSTDIR\libprotobufd.dll"
Delete "$INSTDIR\libxml2d.dll"
Delete "$INSTDIR\pcre2-8d.dll"
Delete "$INSTDIR\pcre2-16d.dll"
@@ -939,15 +1044,25 @@ Section "Uninstall"
Delete "$INSTDIR\zlibd.dll"
!endif
!ifdef arch_x86
Delete "$INSTDIR\libgcc_s_sjlj-1.dll"
Delete "$INSTDIR\libwinpthread-1.dll"
!endif
!endif ; MSVC
; Common files
Delete "$INSTDIR\icudt72.dll"
Delete "$INSTDIR\icudt73.dll"
Delete "$INSTDIR\libfftw3-3.dll"
!ifdef debug
Delete "$INSTDIR\libprotobufd.dll"
!else
Delete "$INSTDIR\libprotobuf.dll"
!endif
!ifdef msvc && debug
Delete "$INSTDIR\icuin72d.dll"
Delete "$INSTDIR\icuuc72d.dll"
Delete "$INSTDIR\icuin73d.dll"
Delete "$INSTDIR\icuuc73d.dll"
Delete "$INSTDIR\Qt6Concurrentd.dll"
Delete "$INSTDIR\Qt6Cored.dll"
Delete "$INSTDIR\Qt6Guid.dll"
@@ -955,8 +1070,8 @@ Section "Uninstall"
Delete "$INSTDIR\Qt6Sqld.dll"
Delete "$INSTDIR\Qt6Widgetsd.dll"
!else
Delete "$INSTDIR\icuin72.dll"
Delete "$INSTDIR\icuuc72.dll"
Delete "$INSTDIR\icuin73.dll"
Delete "$INSTDIR\icuuc73.dll"
Delete "$INSTDIR\Qt6Concurrent.dll"
Delete "$INSTDIR\Qt6Core.dll"
Delete "$INSTDIR\Qt6Gui.dll"
@@ -1114,6 +1229,7 @@ Section "Uninstall"
Delete "$INSTDIR\gstreamer-plugins\gstvolume.dll"
Delete "$INSTDIR\gstreamer-plugins\gstvorbis.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwasapi.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwasapi2.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwavenc.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwavpack.dll"
Delete "$INSTDIR\gstreamer-plugins\gstwavparse.dll"

View File

@@ -50,10 +50,14 @@ enum {
} // namespace
#define gst_fastspectrum_parent_class parent_class
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
#endif
G_DEFINE_TYPE(GstFastSpectrum, gst_fastspectrum, GST_TYPE_AUDIO_FILTER)
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
static void gst_fastspectrum_finalize(GObject *object);
static void gst_fastspectrum_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);

View File

@@ -37,9 +37,7 @@
#include <QString>
#include <QStringList>
#include <QAtomicInt>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
#endif
#include <QRandomGenerator>
#include "core/logging.h"
@@ -165,10 +163,10 @@ class WorkerPool : public _WorkerPoolBase {
template<typename HandlerType>
WorkerPool<HandlerType>::WorkerPool(QObject *parent)
: _WorkerPoolBase(parent),
worker_count_(1),
next_worker_(0),
next_id_(0) {
worker_count_ = qBound(1, QThread::idealThreadCount() / 2, 4);
local_server_name_ = qApp->applicationName().toLower();
if (local_server_name_.isEmpty()) {
@@ -228,7 +226,7 @@ void WorkerPool<HandlerType>::SetExecutableName(const QString &executable_name)
template<typename HandlerType>
void WorkerPool<HandlerType>::Start() {
QMetaObject::invokeMethod(this, "DoStart");
QMetaObject::invokeMethod(this, &WorkerPool<HandlerType>::DoStart);
}
template<typename HandlerType>
@@ -294,11 +292,7 @@ void WorkerPool<HandlerType>::StartOneWorker(Worker *worker) {
// Create a server, find an unused name and start listening
forever {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
const quint32 unique_number = QRandomGenerator::global()->bounded(static_cast<quint32>(quint64(this) & 0xFFFFFFFF));
#else
const quint32 unique_number = qrand() ^ (static_cast<quint32>(quint64(this) & 0xFFFFFFFF));
#endif
const QString name = QString("%1_%2").arg(local_server_name_).arg(unique_number);
if (worker->local_server_->listen(name)) {
@@ -423,7 +417,7 @@ WorkerPool<HandlerType>::SendMessageWithReply(MessageType *message) {
}
// Wake up the main thread
QMetaObject::invokeMethod(this, "SendQueuedMessages", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, &WorkerPool<HandlerType>::SendQueuedMessages, Qt::QueuedConnection);
return reply;

View File

@@ -1,7 +1,15 @@
cmake_minimum_required(VERSION 3.7)
set(MESSAGES tagreadermessages.proto)
set(SOURCES tagreaderbase.cpp)
# Workaround a bug in protobuf-generate.cmake (https://github.com/protocolbuffers/protobuf/issues/12450)
if(NOT protobuf_PROTOC_EXE)
set(protobuf_PROTOC_EXE "protobuf::protoc")
endif()
if(NOT Protobuf_LIBRARIES)
set(Protobuf_LIBRARIES protobuf::libprotobuf protobuf::libprotoc)
endif()
set(SOURCES tagreaderbase.cpp tagreadermessages.proto)
if(USE_TAGLIB AND TAGLIB_FOUND)
list(APPEND SOURCES tagreadertaglib.cpp tagreadergme.cpp)
@@ -11,8 +19,6 @@ if(USE_TAGPARSER AND TAGPARSER_FOUND)
list(APPEND SOURCES tagreadertagparser.cpp)
endif()
protobuf_generate_cpp(PROTO_SOURCES PROTO_HEADERS ${MESSAGES})
link_directories(
${GLIB_LIBRARY_DIRS}
${PROTOBUF_LIBRARY_DIRS}
@@ -43,9 +49,10 @@ target_include_directories(libstrawberry-tagreader PRIVATE
target_link_libraries(libstrawberry-tagreader PRIVATE
${GLIB_LIBRARIES}
${PROTOBUF_LIBRARY}
${Protobuf_LIBRARIES}
${QtCore_LIBRARIES}
${QtNetwork_LIBRARIES}
${QtGui_LIBRARIES}
libstrawberry-common
)
@@ -58,3 +65,5 @@ if(USE_TAGPARSER AND TAGPARSER_FOUND)
target_include_directories(libstrawberry-tagreader SYSTEM PRIVATE ${TAGPARSER_INCLUDE_DIRS})
target_link_libraries(libstrawberry-tagreader PRIVATE ${TAGPARSER_LIBRARIES})
endif()
protobuf_generate(TARGET libstrawberry-tagreader)

View File

@@ -1,5 +1,5 @@
/* This file is part of Strawberry.
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
Copyright 2018-2023, 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
@@ -19,19 +19,20 @@
#include <string>
#include "tagreaderbase.h"
#include <QByteArray>
#include <QString>
#include <QIODevice>
#include <QFile>
#include <QBuffer>
#include <QImage>
#include <QMimeDatabase>
const std::string TagReaderBase::kEmbeddedCover = "(embedded)";
#include "core/logging.h"
#include "tagreaderbase.h"
TagReaderBase::TagReaderBase() = default;
TagReaderBase::~TagReaderBase() = default;
void TagReaderBase::Decode(const QString &tag, std::string *output) {
output->assign(DataCommaSizeFromQString(tag));
}
float TagReaderBase::ConvertPOPMRating(const int POPM_rating) {
if (POPM_rating < 0x01) return 0.0F;
@@ -55,3 +56,88 @@ int TagReaderBase::ConvertToPOPMRating(const float rating) {
return 0xFF;
}
TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const spb::tagreader::SaveFileRequest &request) {
if (!request.has_save_cover() || !request.save_cover()) {
return Cover();
}
const QString song_filename = QString::fromUtf8(request.filename().data(), static_cast<qint64>(request.filename().size()));
QString cover_filename;
if (request.has_cover_filename()) {
cover_filename = QString::fromUtf8(request.cover_filename().data(), static_cast<qint64>(request.cover_filename().size()));
}
QByteArray cover_data;
if (request.has_cover_data()) {
cover_data = QByteArray(request.cover_data().data(), static_cast<qint64>(request.cover_data().size()));
}
QString cover_mime_type;
if (request.has_cover_mime_type()) {
cover_mime_type = QByteArray(request.cover_mime_type().data(), static_cast<qint64>(request.cover_mime_type().size()));
}
return LoadCoverFromRequest(song_filename, cover_filename, cover_data, cover_mime_type);
}
TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request) {
const QString song_filename = QString::fromUtf8(request.filename().data(), static_cast<qint64>(request.filename().size()));
QString cover_filename;
if (request.has_cover_filename()) {
cover_filename = QString::fromUtf8(request.cover_filename().data(), static_cast<qint64>(request.cover_filename().size()));
}
QByteArray cover_data;
if (request.has_cover_data()) {
cover_data = QByteArray(request.cover_data().data(), static_cast<qint64>(request.cover_data().size()));
}
QString cover_mime_type;
if (request.has_cover_mime_type()) {
cover_mime_type = QByteArray(request.cover_mime_type().data(), static_cast<qint64>(request.cover_mime_type().size()));
}
return LoadCoverFromRequest(song_filename, cover_filename, cover_data, cover_mime_type);
}
TagReaderBase::Cover TagReaderBase::LoadCoverFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, QString cover_mime_type) {
if (cover_data.isEmpty() && !cover_filename.isEmpty()) {
qLog(Debug) << "Loading cover from" << cover_filename << "for" << song_filename;
QFile file(cover_filename);
if (!file.open(QIODevice::ReadOnly)) {
qLog(Error) << "Failed to open file" << cover_filename << "for reading:" << file.errorString();
return Cover();
}
cover_data = file.readAll();
file.close();
}
if (!cover_data.isEmpty()) {
if (cover_mime_type.isEmpty()) {
cover_mime_type = QMimeDatabase().mimeTypeForData(cover_data).name();
}
if (cover_mime_type == "image/jpeg") {
qLog(Debug) << "Using cover from JPEG data for" << song_filename;
return Cover(cover_data, cover_mime_type);
}
if (cover_mime_type == "image/png") {
qLog(Debug) << "Using cover from PNG data for" << song_filename;
return Cover(cover_data, cover_mime_type);
}
// Convert image to JPEG.
qLog(Debug) << "Converting cover to JPEG data for" << song_filename;
QImage cover_image(cover_data);
cover_data.clear();
QBuffer buffer(&cover_data);
if (buffer.open(QIODevice::WriteOnly)) {
cover_image.save(&buffer, "JPEG");
buffer.close();
}
return Cover(cover_data, "image/jpeg");
}
return Cover();
}

View File

@@ -1,5 +1,5 @@
/* This file is part of Strawberry.
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
Copyright 2018-2023, 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
@@ -27,9 +27,6 @@
#include "tagreadermessages.pb.h"
#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size())
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
/*
* This class holds all useful methods to read and write tags from/to files.
* You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient)
@@ -39,24 +36,33 @@ class TagReaderBase {
explicit TagReaderBase();
~TagReaderBase();
class Cover {
public:
explicit Cover(const QByteArray &_data = QByteArray(), const QString &_mime_type = QString()) : data(_data), mime_type(_mime_type) {}
QByteArray data;
QString mime_type;
QString error;
};
virtual bool IsMediaFile(const QString &filename) const = 0;
virtual bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0;
virtual bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
virtual bool SaveFile(const spb::tagreader::SaveFileRequest &request) const = 0;
virtual QByteArray LoadEmbeddedArt(const QString &filename) const = 0;
virtual bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) = 0;
virtual bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const = 0;
virtual bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
virtual bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
static void Decode(const QString &tag, std::string *output);
static float ConvertPOPMRating(const int POPM_rating);
static int ConvertToPOPMRating(const float rating);
protected:
static const std::string kEmbeddedCover;
static Cover LoadCoverFromRequest(const spb::tagreader::SaveFileRequest &request);
static Cover LoadCoverFromRequest(const spb::tagreader::SaveEmbeddedArtRequest &request);
private:
static Cover LoadCoverFromRequest(const QString &song_filename, const QString &cover_filename, QByteArray cover_data, QString cover_mime_type);
Q_DISABLE_COPY(TagReaderBase)
};

View File

@@ -84,7 +84,8 @@ void GME::SPC::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *so
// Make sure to check id6 documentation before changing the read values!
file.seek(HAS_ID6_OFFSET);
bool has_id6 = (file.read(1)[0] == static_cast<char>(xID6_STATUS::ON));
const QByteArray id6_status = file.read(1);
const bool has_id6 = id6_status.length() >= 1 && id6_status[0] == static_cast<char>(xID6_STATUS::ON);
file.seek(SONG_TITLE_OFFSET);
song_info->set_title(QString::fromLatin1(file.read(32)).toStdString());
@@ -156,10 +157,10 @@ void GME::SPC::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *so
TagLib::Tag *tag = ape.tag();
if (!tag) return;
TagReaderTagLib::Decode(tag->artist(), song_info->mutable_artist());
TagReaderTagLib::Decode(tag->album(), song_info->mutable_album());
TagReaderTagLib::Decode(tag->title(), song_info->mutable_title());
TagReaderTagLib::Decode(tag->genre(), song_info->mutable_genre());
TagReaderTagLib::TStringToStdString(tag->artist(), song_info->mutable_artist());
TagReaderTagLib::TStringToStdString(tag->album(), song_info->mutable_album());
TagReaderTagLib::TStringToStdString(tag->title(), song_info->mutable_title());
TagReaderTagLib::TStringToStdString(tag->genre(), song_info->mutable_genre());
song_info->set_track(tag->track());
song_info->set_year(tag->year());
}
@@ -278,7 +279,7 @@ bool TagReaderGME::ReadFile(const QString &filename, spb::tagreader::SongMetadat
return GME::ReadFile(fileinfo, song);
}
bool TagReaderGME::SaveFile(const QString&, const spb::tagreader::SongMetadata&) const {
bool TagReaderGME::SaveFile(const spb::tagreader::SaveFileRequest&) const {
return false;
}
@@ -286,7 +287,7 @@ QByteArray TagReaderGME::LoadEmbeddedArt(const QString&) const {
return QByteArray();
}
bool TagReaderGME::SaveEmbeddedArt(const QString&, const QByteArray&) {
bool TagReaderGME::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest&) const {
return false;
}

View File

@@ -55,14 +55,22 @@ constexpr int XID6_OFFSET = (0x101C0 + 64);
constexpr int NANO_PER_MS = 1000000;
enum xID6_STATUS {
enum class xID6_STATUS {
ON = 0x26,
OFF = 0x27,
OFF = 0x27
};
enum xID6_ID { SongName = 0x01, GameName = 0x02, ArtistName = 0x03 };
enum class xID6_ID {
SongName = 0x01,
GameName = 0x02,
ArtistName = 0x03
};
enum xID6_TYPE { Length = 0x0, String = 0x1, Integer = 0x4 };
enum class xID6_TYPE {
Length = 0x0,
String = 0x1,
Integer = 0x4
};
void Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info);
qint16 GetNextMemAddressAlign32bit(qint16 input);
@@ -99,13 +107,13 @@ class TagReaderGME : public TagReaderBase {
bool IsMediaFile(const QString &filename) const override;
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override;
QByteArray LoadEmbeddedArt(const QString &filename) const override;
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
};
#endif
#endif // TAGREADERGME_H

View File

@@ -69,29 +69,26 @@ message SongMetadata {
optional int64 lastplayed = 29;
optional int64 lastseen = 30;
optional string art_automatic = 31;
optional bool art_embedded = 31;
optional float rating = 32;
optional bool suspicious_tags = 40;
optional string acoustid_id = 33;
optional string acoustid_fingerprint = 34;
}
optional string musicbrainz_album_artist_id = 35; // MusicBrainz Release Artist ID (MUSICBRAINZ_ALBUMARTISTID)
optional string musicbrainz_artist_id = 36; // MusicBrainz Artist ID (MUSICBRAINZ_ARTISTID)
optional string musicbrainz_original_artist_id = 37; // MusicBrainz Original Artist ID (MUSICBRAINZ_ORIGINALARTISTID)
optional string musicbrainz_album_id = 38; // MusicBrainz Release ID (MUSICBRAINZ_ALBUMID)
optional string musicbrainz_original_album_id = 39; // MusicBrainz Original Release ID (MUSICBRAINZ_ORIGINALALBUMID)
optional string musicbrainz_recording_id = 40; // MusicBrainz Recording ID (MUSICBRAINZ_TRACKID)
optional string musicbrainz_track_id = 41; // MusicBrainz Track ID (MUSICBRAINZ_RELEASETRACKID)
optional string musicbrainz_disc_id = 42; // MusicBrainz Disc ID (MUSICBRAINZ_DISCID)
optional string musicbrainz_release_group_id = 43; // MusicBrainz Release Group ID (MUSICBRAINZ_RELEASEGROUPID)
optional string musicbrainz_work_id = 44; // MusicBrainz Work ID (MUSICBRAINZ_WORKID)
message ReadFileRequest {
optional string filename = 1;
}
optional bool suspicious_tags = 50;
message ReadFileResponse {
optional SongMetadata metadata = 1;
}
message SaveFileRequest {
optional string filename = 1;
optional SongMetadata metadata = 2;
}
message SaveFileResponse {
optional bool success = 1;
}
message IsMediaFileRequest {
@@ -100,6 +97,33 @@ message IsMediaFileRequest {
message IsMediaFileResponse {
optional bool success = 1;
optional string error = 2;
}
message ReadFileRequest {
optional string filename = 1;
}
message ReadFileResponse {
optional SongMetadata metadata = 1;
optional string error = 2;
}
message SaveFileRequest {
optional string filename = 1;
optional bool save_tags = 2;
optional bool save_playcount = 3;
optional bool save_rating = 4;
optional bool save_cover = 5;
optional SongMetadata metadata = 6;
optional string cover_filename = 7;
optional bytes cover_data = 8;
optional string cover_mime_type = 9;
}
message SaveFileResponse {
optional bool success = 1;
optional string error = 2;
}
message LoadEmbeddedArtRequest {
@@ -108,15 +132,19 @@ message LoadEmbeddedArtRequest {
message LoadEmbeddedArtResponse {
optional bytes data = 1;
optional string error = 2;
}
message SaveEmbeddedArtRequest {
optional string filename = 1;
optional bytes data = 2;
optional string cover_filename = 2;
optional bytes cover_data = 3;
optional string cover_mime_type = 4;
}
message SaveEmbeddedArtResponse {
optional bool success = 1;
optional string error = 2;
}
message SaveSongPlaycountToFileRequest {
@@ -126,6 +154,7 @@ message SaveSongPlaycountToFileRequest {
message SaveSongPlaycountToFileResponse {
optional bool success = 1;
optional string error = 2;
}
message SaveSongRatingToFileRequest {
@@ -135,6 +164,7 @@ message SaveSongRatingToFileRequest {
message SaveSongRatingToFileResponse {
optional bool success = 1;
optional string error = 2;
}
message Message {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
/* This file is part of Strawberry.
Copyright 2013, David Sansome <me@davidsansome.com>
Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
Copyright 2018-2023, 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
@@ -29,8 +29,12 @@
#include <taglib/tstring.h>
#include <taglib/fileref.h>
#include <taglib/xiphcomment.h>
#include <taglib/flacfile.h>
#include <taglib/mpegfile.h>
#include <taglib/mp4file.h>
#include <taglib/apetag.h>
#include <taglib/apefile.h>
#include <taglib/asffile.h>
#include <taglib/id3v2tag.h>
#include <taglib/popularimeterframe.h>
@@ -51,15 +55,15 @@ class TagReaderTagLib : public TagReaderBase {
bool IsMediaFile(const QString &filename) const override;
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override;
QByteArray LoadEmbeddedArt(const QString &filename) const override;
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
static void Decode(const TagLib::String &tag, std::string *output);
static void TStringToStdString(const TagLib::String &tag, std::string *output);
private:
spb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const;
@@ -67,7 +71,7 @@ class TagReaderTagLib : public TagReaderBase {
void ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
void ParseAPETag(const TagLib::APE::ItemListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
void SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comments, const spb::tagreader::SongMetadata &song) const;
void SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comment, const spb::tagreader::SongMetadata &song) const;
void SaveAPETag(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetTextFrame(const char *id, const QString &value, TagLib::ID3v2::Tag *tag) const;
@@ -80,6 +84,23 @@ class TagReaderTagLib : public TagReaderBase {
static TagLib::ID3v2::PopularimeterFrame *GetPOPMFrameFromTag(TagLib::ID3v2::Tag *tag);
void SetPlaycount(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const;
void SetPlaycount(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetPlaycount(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetPlaycount(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetPlaycount(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetRating(TagLib::Ogg::XiphComment *xiph_comment, const spb::tagreader::SongMetadata &song) const;
void SetRating(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetRating(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetRating(TagLib::MP4::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetRating(TagLib::ASF::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data, const QString &mime_type) const;
void SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data, const QString &mime_type) const;
void SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const;
void SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data, const QString &mime_type) const;
private:
FileRefFactory *factory_;

View File

@@ -22,6 +22,7 @@
#include <string>
#include <memory>
#include <algorithm>
#include <vector>
#include <sys/stat.h>
#include <tagparser/mediafileinfo.h>
@@ -41,7 +42,7 @@
#include "core/logging.h"
#include "core/messagehandler.h"
#include "core/timeconstants.h"
#include "utilities/timeconstants.h"
TagReaderTagParser::TagReaderTagParser() = default;
@@ -79,7 +80,7 @@ bool TagReaderTagParser::IsMediaFile(const QString &filename) const {
}
const auto tracks = taginfo.tracks();
for (const auto track : tracks) {
for (TagParser::AbstractTrack *track : tracks) {
if (track->mediaType() == TagParser::MediaType::Audio) {
taginfo.close();
return true;
@@ -102,16 +103,14 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
if (!fileinfo.exists() || fileinfo.suffix().compare("bak", Qt::CaseInsensitive) == 0) return false;
const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
const QByteArray basefilename = fileinfo.fileName().toUtf8();
song->set_basefilename(DataCommaSizeFromQString(fileinfo.fileName()));
song->set_basefilename(basefilename.constData(), basefilename.size());
song->set_url(url.constData(), url.size());
song->set_filesize(fileinfo.size());
song->set_mtime(fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
song->set_ctime(fileinfo.birthTime().isValid() ? std::max(fileinfo.birthTime().toSecsSinceEpoch(), 0LL) : fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL);
#else
song->set_ctime(fileinfo.created().isValid() ? std::max(fileinfo.created().toSecsSinceEpoch(), 0LL) : fileinfo.lastModified().isValid() ? std::max(fileinfo.lastModified().toSecsSinceEpoch(), 0LL) : 0LL);
#endif
if (song->ctime() <= 0) {
song->set_ctime(song->mtime());
@@ -154,8 +153,8 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
qLog(Debug) << QString::fromStdString(msg.message());
}
const auto tracks = taginfo.tracks();
for (const auto track : tracks) {
std::vector<TagParser::AbstractTrack*> tracks = taginfo.tracks();
for (TagParser::AbstractTrack *track : tracks) {
switch (track->format().general) {
case TagParser::GeneralMediaFormat::Flac:
song->set_filetype(spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_FLAC);
@@ -209,7 +208,7 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
return false;
}
for (const auto tag : taginfo.tags()) {
for (TagParser::Tag *tag : taginfo.tags()) {
song->set_albumartist(tag->value(TagParser::KnownField::AlbumArtist).toString(TagParser::TagTextEncoding::Utf8));
song->set_artist(tag->value(TagParser::KnownField::Artist).toString(TagParser::TagTextEncoding::Utf8));
song->set_album(tag->value(TagParser::KnownField::Album).toString(TagParser::TagTextEncoding::Utf8));
@@ -225,7 +224,7 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
song->set_track(tag->value(TagParser::KnownField::TrackPosition).toInteger());
song->set_disc(tag->value(TagParser::KnownField::DiskPosition).toInteger());
if (!tag->value(TagParser::KnownField::Cover).empty() && tag->value(TagParser::KnownField::Cover).dataSize() > 0) {
song->set_art_automatic(kEmbeddedCover);
song->set_art_embedded(true);
}
const float rating = ConvertPOPMRating(tag->value(TagParser::KnownField::Rating));
if (song->rating() <= 0 && rating > 0.0 && rating <= 1.0) {
@@ -256,11 +255,34 @@ bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
}
bool TagReaderTagParser::SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
bool TagReaderTagParser::SaveFile(const spb::tagreader::SaveFileRequest &request) const {
if (filename.isEmpty()) return false;
if (request.filename().empty()) return false;
qLog(Debug) << "Saving tags to" << filename;
const QString filename = QString::fromUtf8(request.filename().data(), request.filename().size());
const spb::tagreader::SongMetadata song = request.metadata();
const bool save_tags = request.has_save_tags() && request.save_tags();
const bool save_playcount = request.has_save_playcount() && request.save_playcount();
const bool save_rating = request.has_save_rating() && request.save_rating();
const bool save_cover = request.has_save_cover() && request.save_cover();
QStringList save_tags_options;
if (save_tags) {
save_tags_options << "tags";
}
if (save_playcount) {
save_tags_options << "playcount";
}
if (save_rating) {
save_tags_options << "rating";
}
if (save_cover) {
save_tags_options << "embedded cover";
}
qLog(Debug) << "Saving" << save_tags_options.join(", ") << "to" << filename;
const QByteArray cover_data = LoadCoverDataFromRequest(request);
try {
TagParser::MediaFileInfo taginfo;
@@ -295,22 +317,34 @@ bool TagReaderTagParser::SaveFile(const QString &filename, const spb::tagreader:
taginfo.createAppropriateTags();
}
for (const auto tag : taginfo.tags()) {
tag->setValue(TagParser::KnownField::AlbumArtist, TagParser::TagValue(song.albumartist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Artist, TagParser::TagValue(song.artist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Album, TagParser::TagValue(song.album(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Title, TagParser::TagValue(song.title(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Genre, TagParser::TagValue(song.genre(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Composer, TagParser::TagValue(song.composer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Performers, TagParser::TagValue(song.performer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Grouping, TagParser::TagValue(song.grouping(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Comment, TagParser::TagValue(song.comment(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Lyrics, TagParser::TagValue(song.lyrics(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::TrackPosition, TagParser::TagValue(song.track()));
tag->setValue(TagParser::KnownField::DiskPosition, TagParser::TagValue(song.disc()));
tag->setValue(TagParser::KnownField::RecordDate, TagParser::TagValue(song.year()));
tag->setValue(TagParser::KnownField::ReleaseDate, TagParser::TagValue(song.originalyear()));
for (TagParser::Tag *tag : taginfo.tags()) {
if (save_tags) {
tag->setValue(TagParser::KnownField::AlbumArtist, TagParser::TagValue(song.albumartist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Artist, TagParser::TagValue(song.artist(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Album, TagParser::TagValue(song.album(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Title, TagParser::TagValue(song.title(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Genre, TagParser::TagValue(song.genre(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Composer, TagParser::TagValue(song.composer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Performers, TagParser::TagValue(song.performer(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Grouping, TagParser::TagValue(song.grouping(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Comment, TagParser::TagValue(song.comment(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::Lyrics, TagParser::TagValue(song.lyrics(), TagParser::TagTextEncoding::Utf8, tag->proposedTextEncoding()));
tag->setValue(TagParser::KnownField::TrackPosition, TagParser::TagValue(song.track()));
tag->setValue(TagParser::KnownField::DiskPosition, TagParser::TagValue(song.disc()));
tag->setValue(TagParser::KnownField::RecordDate, TagParser::TagValue(song.year()));
tag->setValue(TagParser::KnownField::ReleaseDate, TagParser::TagValue(song.originalyear()));
}
if (save_playcount) {
SaveSongPlaycountToFile(tag, song);
}
if (save_rating) {
SaveSongRatingToFile(tag, song);
}
if (save_cover) {
SaveEmbeddedArt(tag, cover_data);
}
}
taginfo.applyChanges(diag, progress);
taginfo.close();
@@ -358,7 +392,7 @@ QByteArray TagReaderTagParser::LoadEmbeddedArt(const QString &filename) const {
return QByteArray();
}
for (const auto tag : taginfo.tags()) {
for (TagParser::Tag *tag : taginfo.tags()) {
if (!tag->value(TagParser::KnownField::Cover).empty() && tag->value(TagParser::KnownField::Cover).dataSize() > 0) {
QByteArray data(tag->value(TagParser::KnownField::Cover).dataPointer(), tag->value(TagParser::KnownField::Cover).dataSize());
taginfo.close();
@@ -379,12 +413,22 @@ QByteArray TagReaderTagParser::LoadEmbeddedArt(const QString &filename) const {
}
bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArray &data) {
void TagReaderTagParser::SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const {
if (filename.isEmpty()) return false;
tag->setValue(TagParser::KnownField::Cover, TagParser::TagValue(data.toStdString()));
}
bool TagReaderTagParser::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const {
if (request.filename().empty()) return false;
const QString filename = QString::fromUtf8(request.filename().data(), request.filename().size());
qLog(Debug) << "Saving art to" << filename;
const QByteArray cover_data = LoadCoverDataFromRequest(request);
try {
TagParser::MediaFileInfo taginfo;
@@ -415,8 +459,8 @@ bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArr
taginfo.createAppropriateTags();
}
for (const auto tag : taginfo.tags()) {
tag->setValue(TagParser::KnownField::Cover, TagParser::TagValue(data.toStdString()));
for (TagParser::Tag *tag : taginfo.tags()) {
SaveEmbeddedArt(tag, cover_data);
}
taginfo.applyChanges(diag, progress);
@@ -435,8 +479,16 @@ bool TagReaderTagParser::SaveEmbeddedArt(const QString &filename, const QByteArr
}
void TagReaderTagParser::SaveSongPlaycountToFile(TagParser::Tag*, const spb::tagreader::SongMetadata&) const {}
bool TagReaderTagParser::SaveSongPlaycountToFile(const QString&, const spb::tagreader::SongMetadata&) const { return false; }
void TagReaderTagParser::SaveSongRatingToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const {
tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(ConvertToPOPMRating(song.rating())));
}
bool TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const {
if (filename.isEmpty()) return false;
@@ -476,9 +528,10 @@ bool TagReaderTagParser::SaveSongRatingToFile(const QString &filename, const spb
taginfo.createAppropriateTags();
}
for (const auto tag : taginfo.tags()) {
tag->setValue(TagParser::KnownField::Rating, TagParser::TagValue(ConvertToPOPMRating(song.rating())));
for (TagParser::Tag *tag : taginfo.tags()) {
SaveSongRatingToFile(tag, song);
}
taginfo.applyChanges(diag, progress);
taginfo.close();

View File

@@ -22,6 +22,8 @@
#include <string>
#include <tagparser/tag.h>
#include <QByteArray>
#include <QString>
@@ -40,14 +42,20 @@ class TagReaderTagParser : public TagReaderBase {
bool IsMediaFile(const QString &filename) const override;
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveFile(const spb::tagreader::SaveFileRequest &request) const override;
QByteArray LoadEmbeddedArt(const QString &filename) const override;
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
bool SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtRequest &request) const override;
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
private:
void SaveSongPlaycountToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SaveSongRatingToFile(TagParser::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SaveEmbeddedArt(TagParser::Tag *tag, const QByteArray &data) const;
public:
Q_DISABLE_COPY(TagReaderTagParser)
};

View File

@@ -20,9 +20,6 @@
#include <QtGlobal>
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
# include <sys/time.h>
#endif
#include <iostream>
#include <QCoreApplication>
@@ -46,13 +43,6 @@ int main(int argc, char **argv) {
return 1;
}
// Seed random number generator
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
timeval time;
gettimeofday(&time, nullptr);
qsrand((time.tv_sec * 1000) + (time.tv_usec / 1000));
#endif
logging::Init();
qLog(Info) << "TagReader worker connecting to" << args[1];
@@ -67,4 +57,5 @@ int main(int argc, char **argv) {
TagReaderWorker worker(&socket);
return a.exec();
}

View File

@@ -56,36 +56,41 @@ void TagReaderWorker::DeviceClosed() {
bool TagReaderWorker::HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply, TagReaderBase *reader) {
if (message.has_is_media_file_request()) {
bool success = reader->IsMediaFile(QStringFromStdString(message.is_media_file_request().filename()));
const QString filename = QString::fromUtf8(message.is_media_file_request().filename().data(), static_cast<qint64>(message.is_media_file_request().filename().size()));
bool success = reader->IsMediaFile(filename);
reply.mutable_is_media_file_response()->set_success(success);
return success;
}
else if (message.has_read_file_request()) {
bool success = reader->ReadFile(QStringFromStdString(message.read_file_request().filename()), reply.mutable_read_file_response()->mutable_metadata());
const QString filename = QString::fromUtf8(message.read_file_request().filename().data(), static_cast<qint64>(static_cast<qint64>(message.read_file_request().filename().size())));
bool success = reader->ReadFile(filename, reply.mutable_read_file_response()->mutable_metadata());
return success;
}
else if (message.has_save_file_request()) {
bool success = reader->SaveFile(QStringFromStdString(message.save_file_request().filename()), message.save_file_request().metadata());
bool success = reader->SaveFile(message.save_file_request());
reply.mutable_save_file_response()->set_success(success);
return success;
}
else if (message.has_load_embedded_art_request()) {
QByteArray data = reader->LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename()));
const QString filename = QString::fromUtf8(message.load_embedded_art_request().filename().data(), static_cast<qint64>(static_cast<qint64>(message.load_embedded_art_request().filename().size())));
QByteArray data = reader->LoadEmbeddedArt(filename);
reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size());
return true;
}
else if (message.has_save_embedded_art_request()) {
bool success = reader->SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), static_cast<qint64>(message.save_embedded_art_request().data().size())));
bool success = reader->SaveEmbeddedArt(message.save_embedded_art_request());
reply.mutable_save_embedded_art_response()->set_success(success);
return success;
}
else if (message.has_save_song_playcount_to_file_request()) {
bool success = reader->SaveSongPlaycountToFile(QStringFromStdString(message.save_song_playcount_to_file_request().filename()), message.save_song_playcount_to_file_request().metadata());
const QString filename = QString::fromUtf8(message.save_song_playcount_to_file_request().filename().data(), static_cast<qint64>(static_cast<qint64>(message.save_song_playcount_to_file_request().filename().size())));
bool success = reader->SaveSongPlaycountToFile(filename, message.save_song_playcount_to_file_request().metadata());
reply.mutable_save_song_playcount_to_file_response()->set_success(success);
return success;
}
else if (message.has_save_song_rating_to_file_request()) {
bool success = reader->SaveSongRatingToFile(QStringFromStdString(message.save_song_rating_to_file_request().filename()), message.save_song_rating_to_file_request().metadata());
const QString filename = QString::fromUtf8(message.save_song_rating_to_file_request().filename().data(), static_cast<qint64>(message.save_song_rating_to_file_request().filename().size()));
bool success = reader->SaveSongRatingToFile(filename, message.save_song_rating_to_file_request().metadata());
reply.mutable_save_song_rating_to_file_response()->set_success(success);
return success;
}

View File

@@ -55,11 +55,14 @@ set(SOURCES
utilities/transliterate.cpp
utilities/xmlutils.cpp
utilities/filemanagerutils.cpp
utilities/coverutils.cpp
utilities/screenutils.cpp
engine/enginetype.cpp
engine/enginebase.cpp
engine/enginedevice.cpp
engine/devicefinders.cpp
engine/devicefinder.cpp
engine/enginemetadata.cpp
analyzer/fht.cpp
analyzer/analyzerbase.cpp
@@ -67,6 +70,7 @@ set(SOURCES
analyzer/blockanalyzer.cpp
analyzer/boomanalyzer.cpp
analyzer/rainbowanalyzer.cpp
analyzer/sonogram.cpp
equalizer/equalizer.cpp
equalizer/equalizerslider.cpp
@@ -143,6 +147,7 @@ set(SOURCES
covermanager/albumcovermanager.cpp
covermanager/albumcovermanagerlist.cpp
covermanager/albumcoverloader.cpp
covermanager/albumcoverloaderoptions.cpp
covermanager/albumcoverfetcher.cpp
covermanager/albumcoverfetchersearch.cpp
covermanager/albumcoversearcher.cpp
@@ -167,15 +172,17 @@ set(SOURCES
lyrics/lyricsproviders.cpp
lyrics/lyricsprovider.cpp
lyrics/lyricssearchrequest.h
lyrics/lyricssearchresult.h
lyrics/lyricsfetcher.cpp
lyrics/lyricsfetchersearch.cpp
lyrics/jsonlyricsprovider.cpp
lyrics/auddlyricsprovider.cpp
lyrics/ovhlyricsprovider.cpp
lyrics/lololyricsprovider.cpp
lyrics/geniuslyricsprovider.cpp
lyrics/musixmatchlyricsprovider.cpp
lyrics/chartlyricsprovider.cpp
lyrics/lyricscomlyricsprovider.cpp
providers/musixmatchprovider.cpp
@@ -202,6 +209,7 @@ set(SOURCES
dialogs/userpassdialog.cpp
dialogs/deleteconfirmationdialog.cpp
dialogs/lastfmimportdialog.cpp
dialogs/messagedialog.cpp
dialogs/snapdialog.cpp
dialogs/saveplaylistsdialog.cpp
@@ -261,10 +269,10 @@ set(SOURCES
radios/radioparadiseservice.cpp
scrobbler/audioscrobbler.cpp
scrobbler/scrobblerservices.cpp
scrobbler/scrobblerservice.cpp
scrobbler/scrobblercache.cpp
scrobbler/scrobblercacheitem.cpp
scrobbler/scrobblemetadata.cpp
scrobbler/scrobblingapi20.cpp
scrobbler/lastfmscrobbler.cpp
scrobbler/librefmscrobbler.cpp
@@ -310,6 +318,7 @@ set(HEADERS
analyzer/blockanalyzer.h
analyzer/boomanalyzer.h
analyzer/rainbowanalyzer.h
analyzer/sonogram.h
equalizer/equalizer.h
equalizer/equalizerslider.h
@@ -405,12 +414,12 @@ set(HEADERS
lyrics/lyricsfetcher.h
lyrics/lyricsfetchersearch.h
lyrics/jsonlyricsprovider.h
lyrics/auddlyricsprovider.h
lyrics/ovhlyricsprovider.h
lyrics/lololyricsprovider.h
lyrics/geniuslyricsprovider.h
lyrics/musixmatchlyricsprovider.h
lyrics/chartlyricsprovider.h
lyrics/lyricscomlyricsprovider.h
settings/settingsdialog.h
settings/settingspage.h
@@ -435,6 +444,7 @@ set(HEADERS
dialogs/userpassdialog.h
dialogs/deleteconfirmationdialog.h
dialogs/lastfmimportdialog.h
dialogs/messagedialog.h
dialogs/snapdialog.h
dialogs/saveplaylistsdialog.h
@@ -492,10 +502,8 @@ set(HEADERS
radios/radioparadiseservice.h
scrobbler/audioscrobbler.h
scrobbler/scrobblerservices.h
scrobbler/scrobblerservice.h
scrobbler/scrobblercache.h
scrobbler/scrobblercacheitem.h
scrobbler/scrobblingapi20.h
scrobbler/lastfmscrobbler.h
scrobbler/librefmscrobbler.h
@@ -562,7 +570,7 @@ set(UI
dialogs/addstreamdialog.ui
dialogs/userpassdialog.ui
dialogs/lastfmimportdialog.ui
dialogs/snapdialog.ui
dialogs/messagedialog.ui
dialogs/saveplaylistsdialog.ui
widgets/trackslider.ui
@@ -830,6 +838,7 @@ optional_source(WIN32
HEADERS
core/windows7thumbbar.h
)
optional_source(MSVC SOURCES engine/uwpdevicefinder.cpp)
optional_source(HAVE_SUBSONIC
SOURCES
@@ -962,8 +971,8 @@ link_directories(
${GOBJECT_LIBRARY_DIRS}
${GNUTLS_LIBRARY_DIRS}
${SQLITE_LIBRARY_DIRS}
${PROTOBUF_LIBRARY_DIRS}
${SINGLEAPPLICATION_LIBRARY_DIRS}
${SINGLECOREAPPLICATION_LIBRARY_DIRS}
)
if(HAVE_ICU)
@@ -1071,7 +1080,6 @@ target_include_directories(strawberry_lib PUBLIC
${CMAKE_SOURCE_DIR}/ext/libstrawberry-tagreader
${CMAKE_BINARY_DIR}/ext/libstrawberry-tagreader
${SINGLEAPPLICATION_INCLUDE_DIRS}
${SINGLECOREAPPLICATION_INCLUDE_DIRS}
)
target_link_libraries(strawberry_lib PUBLIC
@@ -1081,8 +1089,8 @@ target_link_libraries(strawberry_lib PUBLIC
${GNUTLS_LIBRARIES}
${SQLITE_LIBRARIES}
${QT_LIBRARIES}
${Protobuf_LIBRARIES}
${SINGLEAPPLICATION_LIBRARIES}
${SINGLECOREAPPLICATION_LIBRARIES}
libstrawberry-common
libstrawberry-tagreader
)
@@ -1192,13 +1200,13 @@ endif()
if(WIN32)
target_link_libraries(strawberry_lib PRIVATE dsound dwmapi)
if(MSVC)
target_link_libraries(strawberry_lib PRIVATE sqlite3)
if (GETOPT_INCLUDE_DIRS)
target_include_directories(strawberry_lib SYSTEM PRIVATE ${GETOPT_INCLUDE_DIRS})
endif()
if(GETOPT_LIBRARIES)
target_link_libraries(strawberry_lib PRIVATE ${GETOPT_LIBRARIES})
endif()
target_link_libraries(strawberry_lib PRIVATE WindowsApp)
endif()
if(GETOPT_INCLUDE_DIRS)
target_include_directories(strawberry_lib SYSTEM PRIVATE ${GETOPT_INCLUDE_DIRS})
endif()
if(GETOPT_LIBRARIES)
target_link_libraries(strawberry_lib PRIVATE ${GETOPT_LIBRARIES})
endif()
endif()

View File

@@ -50,7 +50,7 @@
// Make an INSTRUCTIONS file
// can't mod scope in analyze you have to use transform for 2D use setErasePixmap Qt function insetead of m_background
Analyzer::Base::Base(QWidget *parent, const uint scopeSize)
AnalyzerBase::AnalyzerBase(QWidget *parent, const uint scopeSize)
: QWidget(parent),
fht_(new FHT(scopeSize)),
engine_(nullptr),
@@ -63,19 +63,19 @@ Analyzer::Base::Base(QWidget *parent, const uint scopeSize)
}
Analyzer::Base::~Base() {
AnalyzerBase::~AnalyzerBase() {
delete fht_;
}
void Analyzer::Base::showEvent(QShowEvent*) {
void AnalyzerBase::showEvent(QShowEvent*) {
timer_.start(timeout(), this);
}
void Analyzer::Base::hideEvent(QHideEvent*) {
void AnalyzerBase::hideEvent(QHideEvent*) {
timer_.stop();
}
void Analyzer::Base::ChangeTimeout(const int timeout) {
void AnalyzerBase::ChangeTimeout(const int timeout) {
timeout_ = timeout;
if (timer_.isActive()) {
@@ -85,7 +85,7 @@ void Analyzer::Base::ChangeTimeout(const int timeout) {
}
void Analyzer::Base::transform(Scope &scope) {
void AnalyzerBase::transform(Scope &scope) {
QVector<float> aux(fht_->size());
if (static_cast<unsigned long int>(aux.size()) >= scope.size()) {
@@ -102,14 +102,14 @@ void Analyzer::Base::transform(Scope &scope) {
}
void Analyzer::Base::paintEvent(QPaintEvent *e) {
void AnalyzerBase::paintEvent(QPaintEvent *e) {
QPainter p(this);
p.fillRect(e->rect(), palette().color(QPalette::Window));
switch (engine_->state()) {
case Engine::Playing: {
const Engine::Scope &thescope = engine_->scope(timeout_);
case EngineBase::State::Playing: {
const EngineBase::Scope &thescope = engine_->scope(timeout_);
int i = 0;
// convert to mono here - our built in analyzers need mono, but the engines provide interleaved pcm
@@ -126,7 +126,7 @@ void Analyzer::Base::paintEvent(QPaintEvent *e) {
break;
}
case Engine::Paused:
case EngineBase::State::Paused:
is_playing_ = false;
analyze(p, lastscope_, new_frame_);
break;
@@ -140,7 +140,7 @@ void Analyzer::Base::paintEvent(QPaintEvent *e) {
}
int Analyzer::Base::resizeExponent(int exp) {
int AnalyzerBase::resizeExponent(int exp) {
if (exp < 3) {
exp = 3;
@@ -157,7 +157,7 @@ int Analyzer::Base::resizeExponent(int exp) {
}
int Analyzer::Base::resizeForBands(const int bands) {
int AnalyzerBase::resizeForBands(const int bands) {
int exp = 0;
if (bands <= 8) {
@@ -184,7 +184,7 @@ int Analyzer::Base::resizeForBands(const int bands) {
}
void Analyzer::Base::demo(QPainter &p) {
void AnalyzerBase::demo(QPainter &p) {
static int t = 201; // FIXME make static to namespace perhaps
@@ -209,7 +209,7 @@ void Analyzer::Base::demo(QPainter &p) {
}
void Analyzer::interpolate(const Scope &inVec, Scope &outVec) {
void AnalyzerBase::interpolate(const Scope &inVec, Scope &outVec) {
double pos = 0.0;
const double step = static_cast<double>(inVec.size()) / static_cast<double>(outVec.size());
@@ -235,7 +235,7 @@ void Analyzer::interpolate(const Scope &inVec, Scope &outVec) {
}
void Analyzer::initSin(Scope &v, const uint size) {
void AnalyzerBase::initSin(Scope &v, const uint size) {
double step = (M_PI * 2) / size;
double radian = 0;
@@ -247,7 +247,7 @@ void Analyzer::initSin(Scope &v, const uint size) {
}
void Analyzer::Base::timerEvent(QTimerEvent *e) {
void AnalyzerBase::timerEvent(QTimerEvent *e) {
QWidget::timerEvent(e);
if (e->timerId() != timer_.timerId()) {

View File

@@ -39,7 +39,6 @@
#include <QPainter>
#include "analyzer/fht.h"
#include "engine/engine_fwd.h"
#include "engine/enginebase.h"
class QHideEvent;
@@ -47,15 +46,11 @@ class QShowEvent;
class QPaintEvent;
class QTimerEvent;
namespace Analyzer {
using Scope = std::vector<float>;
class Base : public QWidget {
class AnalyzerBase : public QWidget {
Q_OBJECT
public:
~Base() override;
~AnalyzerBase() override;
int timeout() const { return timeout_; }
@@ -66,7 +61,8 @@ class Base : public QWidget {
virtual void framerateChanged() {}
protected:
explicit Base(QWidget*, const uint scopeSize = 7);
using Scope = std::vector<float>;
explicit AnalyzerBase(QWidget*, const uint scopeSize = 7);
void hideEvent(QHideEvent*) override;
void showEvent(QShowEvent*) override;
@@ -80,6 +76,9 @@ class Base : public QWidget {
virtual void analyze(QPainter &p, const Scope&, const bool new_frame) = 0;
virtual void demo(QPainter &p);
void interpolate(const Scope&, Scope&);
void initSin(Scope&, const uint = 6000);
protected:
QBasicTimer timer_;
FHT *fht_;
@@ -91,10 +90,5 @@ class Base : public QWidget {
int timeout_;
};
void interpolate(const Scope&, Scope&);
void initSin(Scope&, const uint = 6000);
} // namespace Analyzer
#endif // ANALYZERBASE_H

View File

@@ -40,10 +40,10 @@
#include "blockanalyzer.h"
#include "boomanalyzer.h"
#include "rainbowanalyzer.h"
#include "sonogram.h"
#include "core/logging.h"
#include "engine/enginebase.h"
#include "engine/enginetype.h"
using namespace std::chrono_literals;
@@ -83,8 +83,9 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
AddAnalyzerType<BlockAnalyzer>();
AddAnalyzerType<BoomAnalyzer>();
AddAnalyzerType<Rainbow::NyanCatAnalyzer>();
AddAnalyzerType<Rainbow::RainbowDashAnalyzer>();
AddAnalyzerType<NyanCatAnalyzer>();
AddAnalyzerType<RainbowDashAnalyzer>();
AddAnalyzerType<Sonogram>();
disable_action_ = context_menu_->addAction(tr("No analyzer"), this, &AnalyzerContainer::DisableAnalyzer);
disable_action_->setCheckable(true);
@@ -102,7 +103,7 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
void AnalyzerContainer::mouseReleaseEvent(QMouseEvent *e) {
if (engine_->type() != Engine::EngineType::GStreamer) {
if (engine_->type() != EngineBase::Type::GStreamer) {
return;
}
@@ -148,7 +149,7 @@ void AnalyzerContainer::ChangeAnalyzer(const int id) {
}
delete current_analyzer_;
current_analyzer_ = qobject_cast<Analyzer::Base*>(instance);
current_analyzer_ = qobject_cast<AnalyzerBase*>(instance);
current_analyzer_->set_engine(engine_);
// Even if it is not supposed to happen, I don't want to get a dbz error
current_framerate_ = current_framerate_ == 0 ? kMediumFramerate : current_framerate_;

View File

@@ -30,15 +30,13 @@
#include <QAction>
#include <QActionGroup>
#include "engine/engine_fwd.h"
#include "engine/enginebase.h"
class QTimer;
class QMouseEvent;
class QWheelEvent;
namespace Analyzer {
class Base;
} // namespace Analyzer
class AnalyzerBase;
class AnalyzerContainer : public QWidget {
Q_OBJECT
@@ -53,7 +51,7 @@ class AnalyzerContainer : public QWidget {
static const char *kSettingsFramerate;
signals:
void WheelEvent(int delta);
void WheelEvent(const int delta);
protected:
void mouseReleaseEvent(QMouseEvent*) override;
@@ -94,7 +92,7 @@ class AnalyzerContainer : public QWidget {
QPoint last_click_pos_;
bool ignore_next_click_;
Analyzer::Base *current_analyzer_;
AnalyzerBase *current_analyzer_;
EngineBase *engine_;
};

View File

@@ -46,7 +46,7 @@ const int BlockAnalyzer::kFadeSize = 90;
const char *BlockAnalyzer::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Block analyzer");
BlockAnalyzer::BlockAnalyzer(QWidget *parent)
: Analyzer::Base(parent, 9),
: AnalyzerBase(parent, 9),
columns_(0),
rows_(0),
y_(0),
@@ -124,7 +124,7 @@ void BlockAnalyzer::framerateChanged() {
determineStep();
}
void BlockAnalyzer::transform(Analyzer::Scope &s) {
void BlockAnalyzer::transform(Scope &s) {
for (uint x = 0; x < s.size(); ++x) s[x] *= 2;
@@ -136,7 +136,7 @@ void BlockAnalyzer::transform(Analyzer::Scope &s) {
}
void BlockAnalyzer::analyze(QPainter &p, const Analyzer::Scope &s, bool new_frame) {
void BlockAnalyzer::analyze(QPainter &p, const Scope &s, bool new_frame) {
// y = 2 3 2 1 0 2
// . . . . # .
@@ -158,7 +158,7 @@ void BlockAnalyzer::analyze(QPainter &p, const Analyzer::Scope &s, bool new_fram
QPainter canvas_painter(&canvas_);
Analyzer::interpolate(s, scope_);
interpolate(s, scope_);
// Paint the background
canvas_painter.drawPixmap(0, 0, background_);

View File

@@ -37,7 +37,7 @@
class QWidget;
class QResizeEvent;
class BlockAnalyzer : public Analyzer::Base {
class BlockAnalyzer : public AnalyzerBase {
Q_OBJECT
public:
@@ -53,8 +53,8 @@ class BlockAnalyzer : public Analyzer::Base {
static const char *kName;
protected:
void transform(Analyzer::Scope&) override;
void analyze(QPainter &p, const Analyzer::Scope&, bool new_frame) override;
void transform(Scope&) override;
void analyze(QPainter &p, const Scope&, bool new_frame) override;
void resizeEvent(QResizeEvent*) override;
virtual void paletteChange(const QPalette&);
void framerateChanged() override;
@@ -71,7 +71,7 @@ class BlockAnalyzer : public Analyzer::Base {
QPixmap topbarpixmap_;
QPixmap background_;
QPixmap canvas_;
Analyzer::Scope scope_; // so we don't create a vector every frame
Scope scope_; // so we don't create a vector every frame
QVector<double> store_; // current bar heights
QVector<double> yscale_;

View File

@@ -32,13 +32,10 @@
#include <QPalette>
#include <QColor>
#include "engine/engine_fwd.h"
#include "engine/enginebase.h"
#include "fht.h"
#include "analyzerbase.h"
using Analyzer::Scope;
const int BoomAnalyzer::kColumnWidth = 4;
const int BoomAnalyzer::kMaxBandCount = 256;
const int BoomAnalyzer::kMinBandCount = 32;
@@ -46,7 +43,7 @@ const int BoomAnalyzer::kMinBandCount = 32;
const char *BoomAnalyzer::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Boom analyzer");
BoomAnalyzer::BoomAnalyzer(QWidget *parent)
: Analyzer::Base(parent, 9),
: AnalyzerBase(parent, 9),
bands_(0),
scope_(kMinBandCount),
fg_(palette().color(QPalette::Highlight)),
@@ -110,7 +107,7 @@ void BoomAnalyzer::transform(Scope &s) {
void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame) {
if (!new_frame || engine_->state() == Engine::Paused) {
if (!new_frame || engine_->state() == EngineBase::State::Paused) {
p.drawPixmap(0, 0, canvas_);
return;
}
@@ -120,7 +117,7 @@ void BoomAnalyzer::analyze(QPainter &p, const Scope &scope, const bool new_frame
QPainter canvas_painter(&canvas_);
canvas_.fill(palette().color(QPalette::Window));
Analyzer::interpolate(scope, scope_);
interpolate(scope, scope_);
for (int i = 0, x = 0, y = 0; i < bands_; ++i, x += kColumnWidth + 1) {
h = log10(scope_[i] * 256.0) * F_;

View File

@@ -36,7 +36,7 @@
class QWidget;
class QResizeEvent;
class BoomAnalyzer : public Analyzer::Base {
class BoomAnalyzer : public AnalyzerBase {
Q_OBJECT
public:
@@ -44,8 +44,8 @@ class BoomAnalyzer : public Analyzer::Base {
static const char *kName;
void transform(Analyzer::Scope &s) override;
void analyze(QPainter &p, const Analyzer::Scope&, const bool new_frame) override;
void transform(Scope &s) override;
void analyze(QPainter &p, const Scope&, const bool new_frame) override;
public slots:
void changeK_barHeight(int);
@@ -59,7 +59,7 @@ class BoomAnalyzer : public Analyzer::Base {
static const int kMinBandCount;
int bands_;
Analyzer::Scope scope_;
Scope scope_;
QColor fg_;
double K_barHeight_, F_peakSpeed_, F_;

View File

@@ -41,23 +41,21 @@
#include "fht.h"
#include "analyzerbase.h"
using Analyzer::Scope;
const int RainbowAnalyzer::kHeight[] = { 21, 33 };
const int RainbowAnalyzer::kWidth[] = { 34, 53 };
const int RainbowAnalyzer::kFrameCount[] = { 6, 16 };
const int RainbowAnalyzer::kRainbowHeight[] = { 21, 16 };
const int RainbowAnalyzer::kRainbowOverlap[] = { 13, 15 };
const int RainbowAnalyzer::kSleepingHeight[] = { 24, 33 };
const int Rainbow::RainbowAnalyzer::kHeight[] = { 21, 33 };
const int Rainbow::RainbowAnalyzer::kWidth[] = { 34, 53 };
const int Rainbow::RainbowAnalyzer::kFrameCount[] = { 6, 16 };
const int Rainbow::RainbowAnalyzer::kRainbowHeight[] = { 21, 16 };
const int Rainbow::RainbowAnalyzer::kRainbowOverlap[] = { 13, 15 };
const int Rainbow::RainbowAnalyzer::kSleepingHeight[] = { 24, 33 };
const char *NyanCatAnalyzer::kName = "Nyanalyzer Cat";
const char *RainbowDashAnalyzer::kName = "Rainbow Dash";
const float RainbowAnalyzer::kPixelScale = 0.02F;
const char *Rainbow::NyanCatAnalyzer::kName = "Nyanalyzer Cat";
const char *Rainbow::RainbowDashAnalyzer::kName = "Rainbow Dash";
const float Rainbow::RainbowAnalyzer::kPixelScale = 0.02F;
RainbowAnalyzer::RainbowType RainbowAnalyzer::rainbowtype;
Rainbow::RainbowAnalyzer::RainbowType Rainbow::RainbowAnalyzer::rainbowtype;
Rainbow::RainbowAnalyzer::RainbowAnalyzer(const RainbowType rbtype, QWidget *parent)
: Analyzer::Base(parent, 9),
RainbowAnalyzer::RainbowAnalyzer(const RainbowType rbtype, QWidget *parent)
: AnalyzerBase(parent, 9),
timer_id_(startTimer(kFrameIntervalMs)),
frame_(0),
current_buffer_(0),
@@ -81,20 +79,20 @@ Rainbow::RainbowAnalyzer::RainbowAnalyzer(const RainbowType rbtype, QWidget *par
}
void Rainbow::RainbowAnalyzer::transform(Scope &s) { fht_->spectrum(s.data()); }
void RainbowAnalyzer::transform(Scope &s) { fht_->spectrum(s.data()); }
void Rainbow::RainbowAnalyzer::timerEvent(QTimerEvent *e) {
void RainbowAnalyzer::timerEvent(QTimerEvent *e) {
if (e->timerId() == timer_id_) {
frame_ = (frame_ + 1) % kFrameCount[rainbowtype];
}
else {
Analyzer::Base::timerEvent(e);
AnalyzerBase::timerEvent(e);
}
}
void Rainbow::RainbowAnalyzer::resizeEvent(QResizeEvent *e) {
void RainbowAnalyzer::resizeEvent(QResizeEvent *e) {
Q_UNUSED(e);
@@ -108,7 +106,7 @@ void Rainbow::RainbowAnalyzer::resizeEvent(QResizeEvent *e) {
}
void Rainbow::RainbowAnalyzer::analyze(QPainter &p, const Analyzer::Scope &s, bool new_frame) {
void RainbowAnalyzer::analyze(QPainter &p, const Scope &s, bool new_frame) {
// Discard the second half of the transform
const int scope_size = static_cast<int>(s.size() / 2);
@@ -203,8 +201,8 @@ void Rainbow::RainbowAnalyzer::analyze(QPainter &p, const Analyzer::Scope &s, bo
}
Rainbow::NyanCatAnalyzer::NyanCatAnalyzer(QWidget *parent)
: RainbowAnalyzer(Rainbow::RainbowAnalyzer::Nyancat, parent) {}
NyanCatAnalyzer::NyanCatAnalyzer(QWidget *parent)
: RainbowAnalyzer(RainbowAnalyzer::Nyancat, parent) {}
Rainbow::RainbowDashAnalyzer::RainbowDashAnalyzer(QWidget *parent)
: RainbowAnalyzer(Rainbow::RainbowAnalyzer::Dash, parent) {}
RainbowDashAnalyzer::RainbowDashAnalyzer(QWidget *parent)
: RainbowAnalyzer(RainbowAnalyzer::Dash, parent) {}

View File

@@ -40,8 +40,7 @@ class QWidget;
class QTimerEvent;
class QResizeEvent;
namespace Rainbow {
class RainbowAnalyzer : public Analyzer::Base {
class RainbowAnalyzer : public AnalyzerBase {
Q_OBJECT
public:
@@ -53,8 +52,8 @@ class RainbowAnalyzer : public Analyzer::Base {
RainbowAnalyzer(const RainbowType rbtype, QWidget *parent);
protected:
void transform(Analyzer::Scope&) override;
void analyze(QPainter &p, const Analyzer::Scope&, bool new_frame) override;
void transform(Scope&) override;
void analyze(QPainter &p, const Scope&, bool new_frame) override;
void timerEvent(QTimerEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
@@ -142,6 +141,5 @@ class RainbowDashAnalyzer : public RainbowAnalyzer {
static const char *kName;
};
} // namespace Rainbow
#endif // RAINBOWANALYZER_H

94
src/analyzer/sonogram.cpp Normal file
View File

@@ -0,0 +1,94 @@
/*
Strawberry Music Player
This file was part of Clementine.
Copyright 2004, Melchior FRANZ <mfranz@kde.org>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2010, 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014-2015, Mark Furneaux <mark@furneaux.ca>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.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 <QPainter>
#include <QResizeEvent>
#include "engine/enginebase.h"
#include "sonogram.h"
const char *Sonogram::kName = QT_TRANSLATE_NOOP("AnalyzerContainer", "Sonogram");
Sonogram::Sonogram(QWidget *parent)
: AnalyzerBase(parent, 9) {}
void Sonogram::resizeEvent(QResizeEvent *e) {
Q_UNUSED(e)
canvas_ = QPixmap(size());
canvas_.fill(palette().color(QPalette::Window));
}
void Sonogram::analyze(QPainter &p, const Scope &s, bool new_frame) {
if (!new_frame || engine_->state() == EngineBase::State::Paused) {
p.drawPixmap(0, 0, canvas_);
return;
}
QPainter canvas_painter(&canvas_);
canvas_painter.drawPixmap(0, 0, canvas_, 1, 0, width() - 1, -1);
Scope::const_iterator it = s.begin(), end = s.end();
for (int y = height() - 1; y;) {
QColor c;
if (it >= end || *it < .005) {
c = palette().color(QPalette::Window);
}
else if (*it < .05) {
c.setHsv(95, 255, 255 - static_cast<int>(*it * 4000.0));
}
else if (*it < 1.0) {
c.setHsv(95 - static_cast<int>(*it * 90.0), 255, 255);
}
else {
c = Qt::red;
}
canvas_painter.setPen(c);
canvas_painter.drawPoint(width() - 1, y--);
if (it < end) ++it;
}
canvas_painter.end();
p.drawPixmap(0, 0, canvas_);
}
void Sonogram::transform(Scope &scope) {
fht_->power2(scope.data());
fht_->scale(scope.data(), 1.0 / 256);
scope.resize(fht_->size() / 2);
}
void Sonogram::demo(QPainter &p) {
analyze(p, Scope(fht_->size(), 0), new_frame_);
}

49
src/analyzer/sonogram.h Normal file
View File

@@ -0,0 +1,49 @@
/*
Strawberry Music Player
This file was part of Clementine.
Copyright 2004, Melchior FRANZ <mfranz@kde.org>
Copyright 2009-2010, David Sansome <davidsansome@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Copyright 2015, Mark Furneaux <mark@furneaux.ca>
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 SONOGRAM_H
#define SONOGRAM_H
#include <QPixmap>
#include <QPainter>
#include "analyzerbase.h"
class Sonogram : public AnalyzerBase {
Q_OBJECT
public:
Q_INVOKABLE explicit Sonogram(QWidget *parent);
static const char *kName;
protected:
void resizeEvent(QResizeEvent *e) override;
void analyze(QPainter &p, const Scope &s, bool new_frame) override;
void transform(Scope &scope) override;
void demo(QPainter &p) override;
private:
QPixmap canvas_;
};
#endif // SONOGRAM_H

View File

@@ -58,8 +58,6 @@ SCollection::SCollection(Application *app, QObject *parent)
watcher_(nullptr),
watcher_thread_(nullptr),
original_thread_(nullptr),
io_priority_(Utilities::IoPriority::IOPRIO_CLASS_IDLE),
thread_priority_(QThread::Priority::IdlePriority),
save_playcounts_to_files_(false),
save_ratings_to_files_(false) {
@@ -69,7 +67,7 @@ SCollection::SCollection(Application *app, QObject *parent)
backend()->moveToThread(app->database()->thread());
qLog(Debug) << backend_ << "moved to thread" << app->database()->thread();
backend_->Init(app->database(), app->task_manager(), Song::Source_Collection, kSongsTable, kFtsTable, kDirsTable, kSubdirsTable);
backend_->Init(app->database(), app->task_manager(), Song::Source::Collection, kSongsTable, kFtsTable, kDirsTable, kSubdirsTable);
model_ = new CollectionModel(backend_, app_, this);
@@ -93,20 +91,16 @@ SCollection::~SCollection() {
void SCollection::Init() {
watcher_ = new CollectionWatcher(Song::Source_Collection);
watcher_ = new CollectionWatcher(Song::Source::Collection);
watcher_thread_ = new Thread(this);
#ifndef Q_OS_WIN32
if (io_priority_ != Utilities::IoPriority::IOPRIO_CLASS_NONE) {
watcher_thread_->SetIoPriority(io_priority_);
}
#endif
watcher_thread_->SetIoPriority(Utilities::IoPriority::IOPRIO_CLASS_IDLE);
watcher_->moveToThread(watcher_thread_);
qLog(Debug) << watcher_ << "moved to thread" << watcher_thread_ << "with I/O priority" << io_priority_ << "and thread priority" << thread_priority_;
qLog(Debug) << watcher_ << "moved to thread" << watcher_thread_;
watcher_thread_->start(thread_priority_);
watcher_thread_->start(QThread::IdlePriority);
watcher_->set_backend(backend_);
watcher_->set_task_manager(app_->task_manager());
@@ -169,7 +163,9 @@ void SCollection::AbortScan() { watcher_->Stop(); }
void SCollection::Rescan(const SongList &songs) {
qLog(Debug) << "Rescan" << songs.size() << "songs";
if (!songs.isEmpty()) watcher_->RescanTracksAsync(songs);
if (!songs.isEmpty()) {
watcher_->RescanSongsAsync(songs);
}
}
@@ -184,8 +180,6 @@ void SCollection::ReloadSettings() {
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
io_priority_ = static_cast<Utilities::IoPriority>(s.value("io_priority", Utilities::IOPRIO_CLASS_IDLE).toInt());
thread_priority_ = static_cast<QThread::Priority>(s.value("thread_priority", QThread::Priority::IdlePriority).toInt());
save_playcounts_to_files_ = s.value("save_playcounts", false).toBool();
save_ratings_to_files_ = s.value("save_ratings", false).toBool();
s.endGroup();
@@ -219,9 +213,9 @@ void SCollection::SyncPlaycountAndRatingToFiles() {
}
void SCollection::SongsPlaycountChanged(const SongList &songs) {
void SCollection::SongsPlaycountChanged(const SongList &songs, const bool save_tags) {
if (save_playcounts_to_files_) {
if (save_tags || save_playcounts_to_files_) {
app_->tag_reader_client()->UpdateSongsPlaycount(songs);
}

View File

@@ -28,11 +28,10 @@
#include <QList>
#include <QHash>
#include <QString>
#include <QThread>
#include "core/song.h"
#include "utilities/threadutils.h"
class QThread;
class Application;
class Thread;
class CollectionBackend;
@@ -78,11 +77,11 @@ class SCollection : public QObject {
private slots:
void ExitReceived();
void SongsPlaycountChanged(const SongList &songs);
void SongsPlaycountChanged(const SongList &songs, const bool save_tags = false);
void SongsRatingChanged(const SongList &songs, const bool save_tags = false);
signals:
void Error(QString);
void Error(const QString &error);
void ExitFinished();
private:
@@ -99,8 +98,6 @@ class SCollection : public QObject {
QList<QObject*> wait_for_exit_;
Utilities::IoPriority io_priority_;
QThread::Priority thread_priority_;
bool save_playcounts_to_files_;
bool save_ratings_to_files_;
};

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -60,7 +60,7 @@ CollectionBackend::CollectionBackend(QObject *parent)
: CollectionBackendInterface(parent),
db_(nullptr),
task_manager_(nullptr),
source_(Song::Source_Unknown),
source_(Song::Source::Unknown),
original_thread_(nullptr) {
original_thread_ = thread();
@@ -89,7 +89,7 @@ void CollectionBackend::Close() {
}
void CollectionBackend::ExitAsync() {
QMetaObject::invokeMethod(this, "Exit", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, &CollectionBackend::Exit, Qt::QueuedConnection);
}
void CollectionBackend::Exit() {
@@ -105,31 +105,29 @@ void CollectionBackend::ReportErrors(const CollectionQuery &query) {
const QSqlError sql_error = query.lastError();
if (sql_error.isValid()) {
qLog(Error) << "Unable to execute collection SQL query: " << sql_error;
qLog(Error) << "Faulty SQL query: " << query.lastQuery();
qLog(Error) << "Bound SQL values: " << query.boundValues();
QString error;
error += "Unable to execute collection SQL query: " + sql_error.text() + "<br />";
error += "Faulty SQL query: " + query.lastQuery();
emit Error(error);
qLog(Error) << "Unable to execute collection SQL query:" << sql_error;
qLog(Error) << "Failed SQL query:" << query.lastQuery();
qLog(Error) << "Bound SQL values:" << query.boundValues();
emit Error(tr("Unable to execute collection SQL query: %1").arg(sql_error.text()));
emit Error(tr("Failed SQL query: %1").arg(query.lastQuery()));
}
}
void CollectionBackend::LoadDirectoriesAsync() {
QMetaObject::invokeMethod(this, "LoadDirectories", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, &CollectionBackend::LoadDirectories, Qt::QueuedConnection);
}
void CollectionBackend::UpdateTotalSongCountAsync() {
QMetaObject::invokeMethod(this, "UpdateTotalSongCount", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, &CollectionBackend::UpdateTotalSongCount, Qt::QueuedConnection);
}
void CollectionBackend::UpdateTotalArtistCountAsync() {
QMetaObject::invokeMethod(this, "UpdateTotalArtistCount", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, &CollectionBackend::UpdateTotalArtistCount, Qt::QueuedConnection);
}
void CollectionBackend::UpdateTotalAlbumCountAsync() {
QMetaObject::invokeMethod(this, "UpdateTotalAlbumCount", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, &CollectionBackend::UpdateTotalAlbumCount, Qt::QueuedConnection);
}
void CollectionBackend::IncrementPlayCountAsync(const int id) {
@@ -140,8 +138,12 @@ void CollectionBackend::IncrementSkipCountAsync(const int id, const float progre
QMetaObject::invokeMethod(this, "IncrementSkipCount", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(float, progress));
}
void CollectionBackend::ResetStatisticsAsync(const int id) {
QMetaObject::invokeMethod(this, "ResetStatistics", Qt::QueuedConnection, Q_ARG(int, id));
void CollectionBackend::ResetPlayStatisticsAsync(const int id, const bool save_tags) {
QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(int, id), Q_ARG(bool, save_tags));
}
void CollectionBackend::ResetPlayStatisticsAsync(const QList<int> &id_list, const bool save_tags) {
QMetaObject::invokeMethod(this, "ResetPlayStatistics", Qt::QueuedConnection, Q_ARG(QList<int>, id_list), Q_ARG(bool, save_tags));
}
void CollectionBackend::LoadDirectories() {
@@ -714,7 +716,7 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
Song old_song = old_songs[new_song.song_id()];
if (!new_song.IsMetadataEqual(old_song)) { // Update existing song.
if (!new_song.IsAllMetadataEqual(old_song) || !new_song.IsFingerprintEqual(old_song)) { // Update existing song.
{
SqlQuery q(db);
@@ -1433,7 +1435,7 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
QSqlDatabase db(db_->Connect());
CollectionQuery query(db, songs_table_, fts_table_, opt);
query.SetColumnSpec("url, effective_albumartist, album, compilation_effective, art_automatic, art_manual, filetype, cue_path");
query.SetColumnSpec("url, filetype, cue_path, effective_albumartist, album, compilation_effective, art_embedded, art_automatic, art_manual, art_unset");
query.SetOrderBy("effective_albumartist, album, url");
if (compilation_required) {
@@ -1451,42 +1453,48 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
QMap<QString, Album> albums;
while (query.Next()) {
bool is_compilation = query.Value(3).toBool();
Album info;
Album album_info;
QUrl url = QUrl::fromEncoded(query.Value(0).toByteArray());
album_info.filetype = static_cast<Song::FileType>(query.Value(1).toInt());
const QString filetype = Song::TextForFiletype(album_info.filetype);
album_info.cue_path = query.Value(2).toString();
const bool is_compilation = query.Value(5).toBool();
if (!is_compilation) {
info.album_artist = query.Value(1).toString();
album_info.album_artist = query.Value(3).toString();
}
info.album = query.Value(2).toString();
QString art_automatic = query.Value(4).toString();
album_info.album = query.Value(4).toString();
album_info.art_embedded = query.Value(6).toBool();
const QString art_automatic = query.Value(7).toString();
if (art_automatic.contains(QRegularExpression("..+:.*"))) {
info.art_automatic = QUrl::fromEncoded(art_automatic.toUtf8());
album_info.art_automatic = QUrl::fromEncoded(art_automatic.toUtf8());
}
else {
info.art_automatic = QUrl::fromLocalFile(art_automatic);
album_info.art_automatic = QUrl::fromLocalFile(art_automatic);
}
QString art_manual = query.Value(5).toString();
const QString art_manual = query.Value(8).toString();
if (art_manual.contains(QRegularExpression("..+:.*"))) {
info.art_manual = QUrl::fromEncoded(art_manual.toUtf8());
album_info.art_manual = QUrl::fromEncoded(art_manual.toUtf8());
}
else {
info.art_manual = QUrl::fromLocalFile(art_manual);
album_info.art_manual = QUrl::fromLocalFile(art_manual);
}
info.filetype = static_cast<Song::FileType>(query.Value(6).toInt());
QString filetype = Song::TextForFiletype(info.filetype);
info.cue_path = query.Value(7).toString();
album_info.art_unset = query.Value(9).toBool();
QString key;
if (!info.album_artist.isEmpty()) {
key.append(info.album_artist);
if (!album_info.album_artist.isEmpty()) {
key.append(album_info.album_artist);
}
if (!info.album.isEmpty()) {
if (!album_info.album.isEmpty()) {
if (!key.isEmpty()) key.append("-");
key.append(info.album);
key.append(album_info.album);
}
if (!filetype.isEmpty()) {
key.append(filetype);
@@ -1498,8 +1506,8 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
albums[key].urls.append(url);
}
else {
info.urls << url;
albums.insert(key, info);
album_info.urls << url;
albums.insert(key, album_info);
}
}
@@ -1518,7 +1526,7 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective
ret.album_artist = effective_albumartist;
CollectionQuery query(db, songs_table_, fts_table_);
query.SetColumnSpec("art_automatic, art_manual, url");
query.SetColumnSpec("url, art_embedded, art_automatic, art_manual, art_unset");
if (!effective_albumartist.isEmpty()) {
query.AddWhere("effective_albumartist", effective_albumartist);
}
@@ -1530,22 +1538,24 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective
}
if (query.Next()) {
ret.art_automatic = QUrl::fromEncoded(query.Value(0).toByteArray());
ret.art_manual = QUrl::fromEncoded(query.Value(1).toByteArray());
ret.urls << QUrl::fromEncoded(query.Value(2).toByteArray());
ret.urls << QUrl::fromEncoded(query.Value(0).toByteArray());
ret.art_embedded = query.Value(1).toInt() == 1;
ret.art_automatic = QUrl::fromEncoded(query.Value(2).toByteArray());
ret.art_manual = QUrl::fromEncoded(query.Value(3).toByteArray());
ret.art_unset = query.Value(4).toInt() == 1;
}
return ret;
}
void CollectionBackend::UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic) {
void CollectionBackend::UpdateEmbeddedAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_embedded) {
QMetaObject::invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(QUrl, cover_url), Q_ARG(bool, clear_art_automatic));
QMetaObject::invokeMethod(this, "UpdateEmbeddedAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(bool, art_embedded));
}
void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic) {
void CollectionBackend::UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
@@ -1569,15 +1579,11 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartis
}
// Update the songs
QString sql = QString("UPDATE %1 SET art_manual = :cover").arg(songs_table_);
if (clear_art_automatic) {
sql += ", art_automatic = ''";
}
sql += " WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0";
QString sql = QString("UPDATE %1 SET art_embedded = :art_embedded, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_);
SqlQuery q(db);
q.prepare(sql);
q.BindValue(":cover", cover_url.isValid() ? cover_url.toString(QUrl::FullyEncoded) : "");
q.BindValue(":art_embedded", art_embedded ? 1 : 0);
q.BindValue(":effective_albumartist", effective_albumartist);
q.BindValue(":album", album);
@@ -1606,18 +1612,17 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartis
}
void CollectionBackend::UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual) {
void CollectionBackend::UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &art_manual) {
QMetaObject::invokeMethod(this, "UpdateAutomaticAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(QUrl, cover_url), Q_ARG(bool, clear_art_manual));
QMetaObject::invokeMethod(this, "UpdateManualAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(QUrl, art_manual));
}
void CollectionBackend::UpdateAutomaticAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual) {
void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
// Get the songs before they're updated
CollectionQuery query(db, songs_table_, fts_table_);
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("effective_albumartist", effective_albumartist);
@@ -1635,16 +1640,124 @@ void CollectionBackend::UpdateAutomaticAlbumArt(const QString &effective_albumar
deleted_songs << song;
}
// Update the songs
QString sql = QString("UPDATE %1 SET art_automatic = :cover").arg(songs_table_);
if (clear_art_manual) {
sql += ", art_manual = ''";
}
sql += " WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0";
SqlQuery q(db);
q.prepare(sql);
q.BindValue(":cover", cover_url.isValid() ? cover_url.toString(QUrl::FullyEncoded) : "");
q.prepare(QString("UPDATE %1 SET art_manual = :art_manual, art_unset = 0 WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
q.BindValue(":art_manual", art_manual.isValid() ? art_manual.toString(QUrl::FullyEncoded) : "");
q.BindValue(":effective_albumartist", effective_albumartist);
q.BindValue(":album", album);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
if (!query.Exec()) {
ReportErrors(query);
return;
}
SongList added_songs;
while (query.Next()) {
Song song(source_);
song.InitFromQuery(query, true);
added_songs << song;
}
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
}
}
void CollectionBackend::UnsetAlbumArtAsync(const QString &effective_albumartist, const QString &album) {
QMetaObject::invokeMethod(this, "UnsetAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album));
}
void CollectionBackend::UnsetAlbumArt(const QString &effective_albumartist, const QString &album) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
CollectionQuery query(db, songs_table_, fts_table_);
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("effective_albumartist", effective_albumartist);
query.AddWhere("album", album);
if (!query.Exec()) {
ReportErrors(query);
return;
}
SongList deleted_songs;
while (query.Next()) {
Song song(source_);
song.InitFromQuery(query, true);
deleted_songs << song;
}
SqlQuery q(db);
q.prepare(QString("UPDATE %1 SET art_unset = 1, art_manual = '', art_automatic = '', art_embedded = '' WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
q.BindValue(":effective_albumartist", effective_albumartist);
q.BindValue(":album", album);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
if (!query.Exec()) {
ReportErrors(query);
return;
}
SongList added_songs;
while (query.Next()) {
Song song(source_);
song.InitFromQuery(query, true);
added_songs << song;
}
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
}
}
void CollectionBackend::ClearAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool unset) {
QMetaObject::invokeMethod(this, "ClearAlbumArt", Qt::QueuedConnection, Q_ARG(QString, effective_albumartist), Q_ARG(QString, album), Q_ARG(bool, unset));
}
void CollectionBackend::ClearAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_unset) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
CollectionQuery query(db, songs_table_, fts_table_);
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddWhere("effective_albumartist", effective_albumartist);
query.AddWhere("album", album);
if (!query.Exec()) {
ReportErrors(query);
return;
}
SongList deleted_songs;
while (query.Next()) {
Song song(source_);
song.InitFromQuery(query, true);
deleted_songs << song;
}
SqlQuery q(db);
q.prepare(QString("UPDATE %1 SET art_embedded = 0, art_automatic = '', art_manual = '', art_unset = :art_unset WHERE effective_albumartist = :effective_albumartist AND album = :album AND unavailable = 0").arg(songs_table_));
q.BindValue(":art_unset", art_unset ? 1 : 0);
q.BindValue(":effective_albumartist", effective_albumartist);
q.BindValue(":album", album);
@@ -1653,7 +1766,6 @@ void CollectionBackend::UpdateAutomaticAlbumArt(const QString &effective_albumar
return;
}
// Now get the updated songs
if (!query.Exec()) {
ReportErrors(query);
return;
@@ -1776,29 +1888,54 @@ void CollectionBackend::IncrementSkipCount(const int id, const float progress) {
}
void CollectionBackend::ResetStatistics(const int id) {
void CollectionBackend::ResetPlayStatistics(const int id, const bool save_tags) {
if (id == -1) return;
ResetPlayStatistics(QList<int>() << id, save_tags);
}
void CollectionBackend::ResetPlayStatistics(const QList<int> &id_list, const bool save_tags) {
if (id_list.isEmpty()) return;
QStringList id_str_list;
id_str_list.reserve(id_list.count());
for (const int id : id_list) {
id_str_list << QString::number(id);
}
const bool success = ResetPlayStatistics(id_str_list);
if (success) {
const SongList songs = GetSongsById(id_list);
emit SongsStatisticsChanged(songs, save_tags);
}
}
bool CollectionBackend::ResetPlayStatistics(const QStringList &id_str_list) {
if (id_str_list.isEmpty()) return false;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
SqlQuery q(db);
q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID = :id").arg(songs_table_));
q.BindValue(":id", id);
q.prepare(QString("UPDATE %1 SET playcount = 0, skipcount = 0, lastplayed = -1 WHERE ROWID IN (:ids)").arg(songs_table_));
q.BindValue(":ids", id_str_list.join(","));
if (!q.Exec()) {
db_->ReportErrors(q);
return;
return false;
}
Song new_song = GetSongById(id, db);
emit SongsStatisticsChanged(SongList() << new_song);
return true;
}
void CollectionBackend::DeleteAllAsync() {
QMetaObject::invokeMethod(this, "DeleteAll", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, &CollectionBackend::DeleteAll, Qt::QueuedConnection);
}
@@ -1864,7 +2001,7 @@ SongList CollectionBackend::SmartPlaylistsFindSongs(const SmartPlaylistSearch &s
SongList CollectionBackend::SmartPlaylistsGetAllSongs() {
// Get all the songs!
return SmartPlaylistsFindSongs(SmartPlaylistSearch(SmartPlaylistSearch::Type_All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::Sort_FieldAsc, SmartPlaylistSearchTerm::Field_Artist, -1));
return SmartPlaylistsFindSongs(SmartPlaylistSearch(SmartPlaylistSearch::SearchType::All, SmartPlaylistSearch::TermList(), SmartPlaylistSearch::SortType::FieldAsc, SmartPlaylistSearchTerm::Field::Artist, -1));
}
@@ -1927,7 +2064,7 @@ void CollectionBackend::UpdateLastPlayed(const QString &artist, const QString &a
}
void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &title, const int playcount) {
void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &title, const int playcount, const bool save_tags) {
SongList songs = GetSongsBy(artist, QString(), title);
if (songs.isEmpty()) {
@@ -1949,7 +2086,7 @@ void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &ti
}
}
emit SongsStatisticsChanged(SongList() << songs);
emit SongsStatisticsChanged(SongList() << songs, save_tags);
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2023, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -54,12 +54,14 @@ class CollectionBackendInterface : public QObject {
explicit CollectionBackendInterface(QObject *parent = nullptr) : QObject(parent) {}
struct Album {
Album() : filetype(Song::FileType_Unknown) {}
Album(const QString &_album_artist, const QString &_album, const QUrl &_art_automatic, const QUrl &_art_manual, const QList<QUrl> &_urls, const Song::FileType _filetype, const QString &_cue_path)
Album() : art_embedded(false), art_unset(false), filetype(Song::FileType::Unknown) {}
Album(const QString &_album_artist, const QString &_album, const bool _art_embedded, const QUrl &_art_automatic, const QUrl &_art_manual, const bool _art_unset, const QList<QUrl> &_urls, const Song::FileType _filetype, const QString &_cue_path)
: album_artist(_album_artist),
album(_album),
art_embedded(_art_embedded),
art_automatic(_art_automatic),
art_manual(_art_manual),
art_unset(_art_unset),
urls(_urls),
filetype(_filetype),
cue_path(_cue_path) {}
@@ -67,8 +69,10 @@ class CollectionBackendInterface : public QObject {
QString album_artist;
QString album;
bool art_embedded;
QUrl art_automatic;
QUrl art_manual;
bool art_unset;
QList<QUrl> urls;
Song::FileType filetype;
QString cue_path;
@@ -109,8 +113,10 @@ class CollectionBackendInterface : public QObject {
virtual AlbumList GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
virtual AlbumList GetCompilationAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) = 0;
virtual void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false) = 0;
virtual void UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual = false) = 0;
virtual void UpdateEmbeddedAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_embedded) = 0;
virtual void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &art_manual) = 0;
virtual void UnsetAlbumArtAsync(const QString &effective_albumartist, const QString &album) = 0;
virtual void ClearAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_unset) = 0;
virtual Album GetAlbumArt(const QString &effective_albumartist, const QString &album) = 0;
@@ -179,8 +185,10 @@ class CollectionBackend : public CollectionBackendInterface {
AlbumList GetCompilationAlbums(const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
AlbumList GetAlbumsByArtist(const QString &artist, const CollectionFilterOptions &opt = CollectionFilterOptions()) override;
void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false) override;
void UpdateAutomaticAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual = false) override;
void UpdateEmbeddedAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_embedded) override;
void UpdateManualAlbumArtAsync(const QString &effective_albumartist, const QString &album, const QUrl &art_manual) override;
void UnsetAlbumArtAsync(const QString &effective_albumartist, const QString &album) override;
void ClearAlbumArtAsync(const QString &effective_albumartist, const QString &album, const bool art_unset) override;
Album GetAlbumArt(const QString &effective_albumartist, const QString &album) override;
@@ -200,7 +208,8 @@ class CollectionBackend : public CollectionBackendInterface {
void IncrementPlayCountAsync(const int id);
void IncrementSkipCountAsync(const int id, const float progress);
void ResetStatisticsAsync(const int id);
void ResetPlayStatisticsAsync(const int id, const bool save_tags = false);
void ResetPlayStatisticsAsync(const QList<int> &id_list, const bool save_tags = false);
void DeleteAllAsync();
@@ -231,18 +240,22 @@ class CollectionBackend : public CollectionBackendInterface {
void MarkSongsUnavailable(const SongList &songs, const bool unavailable = true);
void AddOrUpdateSubdirs(const CollectionSubdirectoryList &subdirs);
void CompilationsNeedUpdating();
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_automatic = false);
void UpdateAutomaticAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &cover_url, const bool clear_art_manual = false);
void UpdateEmbeddedAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_embedded);
void UpdateManualAlbumArt(const QString &effective_albumartist, const QString &album, const QUrl &art_manual);
void UnsetAlbumArt(const QString &effective_albumartist, const QString &album);
void ClearAlbumArt(const QString &effective_albumartist, const QString &album, const bool art_unset);
void ForceCompilation(const QString &album, const QList<QString> &artists, const bool on);
void IncrementPlayCount(const int id);
void IncrementSkipCount(const int id, const float progress);
void ResetStatistics(const int id);
void ResetPlayStatistics(const int id, const bool save_tags = false);
void ResetPlayStatistics(const QList<int> &id_list, const bool save_tags = false);
bool ResetPlayStatistics(const QStringList &id_str_list);
void DeleteAll();
void SongPathChanged(const Song &song, const QFileInfo &new_file, const std::optional<int> new_collection_directory_id);
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 UpdatePlayCount(const QString &artist, const QString &title, const int playcount, const bool save_tags = false);
void UpdateSongRating(const int id, const float rating, const bool save_tags = false);
void UpdateSongsRating(const QList<int> &id_list, const float rating, const bool save_tags = false);
@@ -251,23 +264,23 @@ class CollectionBackend : public CollectionBackendInterface {
void ExpireSongs(const int directory_id, const int expire_unavailable_songs_days);
signals:
void DirectoryDiscovered(CollectionDirectory, CollectionSubdirectoryList);
void DirectoryDeleted(CollectionDirectory);
void DirectoryDiscovered(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdir);
void DirectoryDeleted(const CollectionDirectory &dir);
void SongsDiscovered(SongList);
void SongsDeleted(SongList);
void SongsStatisticsChanged(SongList);
void SongsDiscovered(const SongList &songs);
void SongsDeleted(const SongList &songs);
void SongsStatisticsChanged(const SongList &songs, const bool save_tags = false);
void DatabaseReset();
void TotalSongCountUpdated(int);
void TotalArtistCountUpdated(int);
void TotalAlbumCountUpdated(int);
void SongsRatingChanged(SongList, bool);
void TotalSongCountUpdated(const int count);
void TotalArtistCountUpdated(const int count);
void TotalAlbumCountUpdated(const int count);
void SongsRatingChanged(const SongList &songs, const bool save_tags);
void ExitFinished();
void Error(QString);
void Error(const QString &error);
private:
struct CompilationInfo {
@@ -300,7 +313,6 @@ class CollectionBackend : public CollectionBackendInterface {
QString subdirs_table_;
QString fts_table_;
QThread *original_thread_;
};
#endif // COLLECTIONBACKEND_H

View File

@@ -24,7 +24,7 @@
#include "collectionfilteroptions.h"
CollectionFilterOptions::CollectionFilterOptions() : filter_mode_(FilterMode_All), max_age_(-1) {}
CollectionFilterOptions::CollectionFilterOptions() : filter_mode_(FilterMode::All), max_age_(-1) {}
bool CollectionFilterOptions::Matches(const Song &song) const {
@@ -34,7 +34,7 @@ bool CollectionFilterOptions::Matches(const Song &song) const {
}
if (!filter_text_.isNull()) {
return song.artist().contains(filter_text_, Qt::CaseInsensitive) || song.album().contains(filter_text_, Qt::CaseInsensitive) || song.title().contains(filter_text_, Qt::CaseInsensitive);
return song.albumartist().contains(filter_text_, Qt::CaseInsensitive) || song.artist().contains(filter_text_, Qt::CaseInsensitive) || song.album().contains(filter_text_, Qt::CaseInsensitive) || song.title().contains(filter_text_, Qt::CaseInsensitive);
}
return true;

View File

@@ -34,10 +34,10 @@ class CollectionFilterOptions {
// - use the duplicated songs view; by duplicated we mean those songs for which the (artist, album, title) tuple is found more than once in the songs table
// - use the untagged songs view; by untagged we mean those for which at least one of the (artist, album, title) tags is empty
// Please note that additional filtering based on FTS table (the filter attribute) won't work in Duplicates and Untagged modes.
enum FilterMode {
FilterMode_All,
FilterMode_Duplicates,
FilterMode_Untagged
enum class FilterMode {
All,
Duplicates,
Untagged
};
FilterMode filter_mode() const { return filter_mode_; }
@@ -50,7 +50,7 @@ class CollectionFilterOptions {
}
void set_max_age(const int max_age) { max_age_ = max_age; }
void set_filter_text(const QString &filter_text) {
filter_mode_ = FilterMode_All;
filter_mode_ = FilterMode::All;
filter_text_ = filter_text;
}

View File

@@ -54,6 +54,7 @@
#include "groupbydialog.h"
#include "ui_collectionfilterwidget.h"
#include "widgets/qsearchfield.h"
#include "settings/collectionsettingspage.h"
#include "settings/appearancesettingspage.h"
CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
@@ -68,7 +69,7 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
group_by_group_(nullptr),
filter_delay_(new QTimer(this)),
filter_applies_to_model_(true),
delay_behaviour_(DelayedOnLargeLibraries) {
delay_behaviour_(DelayBehaviour::DelayedOnLargeLibraries) {
ui_->setupUi(this);
@@ -178,12 +179,13 @@ void CollectionFilterWidget::Init(CollectionModel *model) {
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_key(1), static_cast<int>(CollectionModel::GroupBy_AlbumArtist)).toInt()),
CollectionModel::GroupBy(s.value(group_by_key(2), static_cast<int>(CollectionModel::GroupBy_AlbumDisc)).toInt()),
CollectionModel::GroupBy(s.value(group_by_key(3), static_cast<int>(CollectionModel::GroupBy_None)).toInt())), s.value(separate_albums_by_grouping_key(), false).toBool());
static_cast<CollectionModel::GroupBy>(s.value(group_by_key(1), static_cast<int>(CollectionModel::GroupBy::AlbumArtist)).toInt()),
static_cast<CollectionModel::GroupBy>(s.value(group_by_key(2), static_cast<int>(CollectionModel::GroupBy::AlbumDisc)).toInt()),
static_cast<CollectionModel::GroupBy>(s.value(group_by_key(3), static_cast<int>(CollectionModel::GroupBy::None)).toInt())),
s.value(separate_albums_by_grouping_key(), false).toBool());
}
else {
model_->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_AlbumDisc, CollectionModel::GroupBy_None), false);
model_->SetGroupBy(CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::AlbumDisc, CollectionModel::GroupBy::None), false);
}
s.endGroup();
}
@@ -272,24 +274,24 @@ QActionGroup *CollectionFilterWidget::CreateGroupByActions(const QString &saved_
QActionGroup *ret = new QActionGroup(parent);
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_Album)));
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_AlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_YearAlbum)));
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_YearAlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::Album)));
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::AlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::YearAlbum)));
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::YearAlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_Album)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_AlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_YearAlbum)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_YearAlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::Album)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::AlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::YearAlbum)));
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album - Disc"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::YearAlbumDisc)));
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_Album)));
ret->addAction(CreateGroupByAction(tr("Group by Genre/Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_Album)));
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Genre, CollectionModel::GroupBy::AlbumArtist, CollectionModel::GroupBy::Album)));
ret->addAction(CreateGroupByAction(tr("Group by Genre/Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Genre, CollectionModel::GroupBy::Artist, CollectionModel::GroupBy::Album)));
ret->addAction(CreateGroupByAction(tr("Group by Album Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist)));
ret->addAction(CreateGroupByAction(tr("Group by Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist)));
ret->addAction(CreateGroupByAction(tr("Group by Album Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::AlbumArtist)));
ret->addAction(CreateGroupByAction(tr("Group by Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Artist)));
ret->addAction(CreateGroupByAction(tr("Group by Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Album)));
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_Album)));
ret->addAction(CreateGroupByAction(tr("Group by Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Album)));
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy::Genre, CollectionModel::GroupBy::Album)));
QAction *sep1 = new QAction(parent);
sep1->setSeparator(true);
@@ -334,7 +336,7 @@ QAction *CollectionFilterWidget::CreateGroupByAction(const QString &text, QObjec
QAction *ret = new QAction(text, parent);
ret->setCheckable(true);
if (grouping.first != CollectionModel::GroupBy_None) {
if (grouping.first != CollectionModel::GroupBy::None) {
ret->setProperty("group_by", QVariant::fromValue(grouping));
}
@@ -459,7 +461,7 @@ void CollectionFilterWidget::SetFilterHint(const QString &hint) {
void CollectionFilterWidget::SetFilterMode(CollectionFilterOptions::FilterMode filter_mode) {
ui_->search_field->clear();
ui_->search_field->setEnabled(filter_mode == CollectionFilterOptions::FilterMode_All);
ui_->search_field->setEnabled(filter_mode == CollectionFilterOptions::FilterMode::All);
model_->SetFilterMode(filter_mode);
@@ -509,7 +511,7 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) {
// Searching with one or two characters can be very expensive on the database even with FTS,
// so if there are a large number of songs in the database introduce a small delay before actually filtering the model,
// so if the user is typing the first few characters of something it will be quicker.
const bool delay = (delay_behaviour_ == AlwaysDelayed) || (delay_behaviour_ == DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
const bool delay = (delay_behaviour_ == DelayBehaviour::AlwaysDelayed) || (delay_behaviour_ == DelayBehaviour::DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
if (delay) {
filter_delay_->start();

View File

@@ -54,7 +54,7 @@ class CollectionFilterWidget : public QWidget {
static const int kFilterDelay = 500; // msec
enum DelayBehaviour {
enum class DelayBehaviour {
AlwaysInstant,
DelayedOnLargeLibraries,
AlwaysDelayed,
@@ -96,7 +96,7 @@ class CollectionFilterWidget : public QWidget {
void UpPressed();
void DownPressed();
void ReturnPressed();
void Filter(QString text);
void Filter(const QString &text);
protected:
void keyReleaseEvent(QKeyEvent *e) override;

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-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2023, 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
@@ -49,11 +49,11 @@
#include "core/song.h"
#include "core/sqlrow.h"
#include "covermanager/albumcoverloader.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "collectionfilteroptions.h"
#include "collectionquery.h"
#include "collectionqueryoptions.h"
#include "collectionitem.h"
#include "covermanager/albumcoverloaderoptions.h"
class QSettings;
@@ -83,34 +83,34 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
};
// These values get saved in QSettings - don't change them
enum GroupBy {
GroupBy_None = 0,
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_Samplerate = 18,
GroupBy_Bitdepth = 19,
GroupBy_Bitrate = 20,
enum class GroupBy {
None = 0,
AlbumArtist = 1,
Artist = 2,
Album = 3,
AlbumDisc = 4,
YearAlbum = 5,
YearAlbumDisc = 6,
OriginalYearAlbum = 7,
OriginalYearAlbumDisc = 8,
Disc = 9,
Year = 10,
OriginalYear = 11,
Genre = 12,
Composer = 13,
Performer = 14,
Grouping = 15,
FileType = 16,
Format = 17,
Samplerate = 18,
Bitdepth = 19,
Bitrate = 20,
GroupByCount = 21,
};
Q_ENUM(GroupBy)
struct Grouping {
explicit Grouping(GroupBy f = GroupBy_None, GroupBy s = GroupBy_None, GroupBy t = GroupBy_None)
explicit Grouping(GroupBy f = GroupBy::None, GroupBy s = GroupBy::None, GroupBy t = GroupBy::None)
: first(f), second(s), third(t) {}
GroupBy first;
@@ -181,9 +181,9 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
quint64 icon_cache_disk_size() { return sIconCache->cacheSize(); }
static bool IsArtistGroupBy(const GroupBy group_by) {
return group_by == CollectionModel::GroupBy_Artist || group_by == CollectionModel::GroupBy_AlbumArtist;
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; }
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; }
@@ -199,10 +199,10 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
static QString ContainerKey(const GroupBy group_by, const bool separate_albums_by_grouping, const Song &song);
signals:
void TotalSongCountUpdated(int count);
void TotalArtistCountUpdated(int count);
void TotalAlbumCountUpdated(int count);
void GroupingChanged(CollectionModel::Grouping g, bool separate_albums_by_grouping);
void TotalSongCountUpdated(const int count);
void TotalArtistCountUpdated(const int count);
void TotalAlbumCountUpdated(const int count);
void GroupingChanged(const CollectionModel::Grouping g, const bool separate_albums_by_grouping);
public slots:
void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode);
@@ -267,6 +267,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
// Helpers
static bool IsCompilationArtistNode(const CollectionItem *node) { return node == node->parent->compilation_artist_node_; }
QString AlbumIconPixmapCacheKey(const QModelIndex &idx) const;
QUrl AlbumIconPixmapDiskCacheKey(const QString &cache_key) const;
QVariant AlbumIcon(const QModelIndex &idx);
QVariant data(const CollectionItem *item, const int role) const;
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
@@ -309,7 +310,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
bool use_disk_cache_;
bool use_lazy_loading_;
AlbumCoverLoaderOptions cover_loader_options_;
AlbumCoverLoaderOptions::Types cover_types_;
using ItemAndCacheKey = QPair<CollectionItem*, QString>;
QMap<quint64, ItemAndCacheKey> pending_art_;

View File

@@ -29,12 +29,12 @@
class SqlRow;
CollectionPlaylistItem::CollectionPlaylistItem() : PlaylistItem(Song::Source_Collection) {
song_.set_source(Song::Source_Collection);
CollectionPlaylistItem::CollectionPlaylistItem() : PlaylistItem(Song::Source::Collection) {
song_.set_source(Song::Source::Collection);
}
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(Song::Source_Collection), song_(song) {
song_.set_source(Song::Source_Collection);
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song) : PlaylistItem(Song::Source::Collection), song_(song) {
song_.set_source(Song::Source::Collection);
}
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
@@ -50,7 +50,7 @@ bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
// Rows from the songs tables come first
song_.InitFromQuery(query, true);
song_.set_source(Song::Source_Collection);
song_.set_source(Song::Source::Collection);
return song_.is_valid();
}

View File

@@ -52,7 +52,7 @@ class CollectionPlaylistItem : public PlaylistItem {
protected:
QVariant DatabaseValue(DatabaseColumn column) const override;
Song DatabaseSongMetadata() const override { return Song(Song::Source_Collection); }
Song DatabaseSongMetadata() const override { return Song(Song::Source::Collection); }
protected:
Song song_;

View File

@@ -113,9 +113,9 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
// Untagged mode could work with additional filtering but I'm disabling it just to be consistent
// this way filtering is available only in the All mode.
// Remember though that when you fix the Duplicates + FTS cooperation, enable the filtering in both Duplicates and Untagged modes.
duplicates_only_ = filter_options.filter_mode() == CollectionFilterOptions::FilterMode_Duplicates;
duplicates_only_ = filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Duplicates;
if (filter_options.filter_mode() == CollectionFilterOptions::FilterMode_Untagged) {
if (filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Untagged) {
where_clauses_ << "(artist = '' OR album = '' OR title ='')";
}

View File

@@ -24,7 +24,7 @@
#include "collectionfilteroptions.h"
CollectionQueryOptions::CollectionQueryOptions()
: compilation_requirement_(false),
: compilation_requirement_(CollectionQueryOptions::CompilationRequirement::None),
query_have_compilations_(false) {}
void CollectionQueryOptions::AddWhere(const QString &column, const QVariant &value, const QString &op) {

View File

@@ -36,12 +36,18 @@ class CollectionQueryOptions {
QString op;
};
enum class CompilationRequirement {
None,
On,
Off
};
QString column_spec() const { return column_spec_; }
bool compilation_requirement() const { return compilation_requirement_; }
CompilationRequirement compilation_requirement() const { return compilation_requirement_; }
bool query_have_compilations() const { return query_have_compilations_; }
void set_column_spec(const QString &column_spec) { column_spec_ = column_spec; }
void set_compilation_requirement(const bool compilation_requirement) { compilation_requirement_ = compilation_requirement; }
void set_compilation_requirement(const CompilationRequirement compilation_requirement) { compilation_requirement_ = compilation_requirement; }
void set_query_have_compilations(const bool query_have_compilations) { query_have_compilations_ = query_have_compilations; }
QList<Where> where_clauses() const { return where_clauses_; }
@@ -49,7 +55,7 @@ class CollectionQueryOptions {
private:
QString column_spec_;
bool compilation_requirement_;
CompilationRequirement compilation_requirement_;
bool query_have_compilations_;
QList<Where> where_clauses_;
};

View File

@@ -117,7 +117,7 @@ void CollectionView::SaveFocus() {
QModelIndex current = currentIndex();
QVariant type = model()->data(current, CollectionModel::Role_Type);
if (!type.isValid() || !(type.toInt() == CollectionItem::Type_Song || type.toInt() == CollectionItem::Type_Container || type.toInt() == CollectionItem::Type_Divider)) {
if (!type.isValid() || (type.toInt() != CollectionItem::Type_Song && type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) {
return;
}
@@ -154,7 +154,7 @@ void CollectionView::SaveContainerPath(const QModelIndex &child) {
QModelIndex current = model()->parent(child);
QVariant type = model()->data(current, CollectionModel::Role_Type);
if (!type.isValid() || !(type.toInt() == CollectionItem::Type_Container || type.toInt() == CollectionItem::Type_Divider)) {
if (!type.isValid() || (type.toInt() != CollectionItem::Type_Container && type.toInt() != CollectionItem::Type_Divider)) {
return;
}
@@ -699,7 +699,7 @@ 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);
dialog->Show(OrganizeErrorDialog::OperationType::Delete, songs_with_errors);
// It deletes itself when the user closes it
}

View File

@@ -89,7 +89,7 @@ class CollectionView : public AutoExpandingTreeView {
void TotalSongCountUpdated_();
void TotalArtistCountUpdated_();
void TotalAlbumCountUpdated_();
void Error(QString);
void Error(const QString &error);
protected:
// QWidget

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2023, 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
@@ -78,13 +78,12 @@ CollectionWatcher::CollectionWatcher(Song::Source source, QObject *parent)
scan_on_startup_(true),
monitor_(true),
song_tracking_(false),
mark_songs_unavailable_(source_ == Song::Source_Collection),
mark_songs_unavailable_(source_ == Song::Source::Collection),
expire_unavailable_songs_days_(60),
overwrite_playcount_(false),
overwrite_rating_(false),
stop_requested_(false),
abort_requested_(false),
rescan_in_progress_(false),
rescan_timer_(new QTimer(this)),
periodic_scan_timer_(new QTimer(this)),
rescan_paused_(false),
@@ -109,13 +108,14 @@ CollectionWatcher::CollectionWatcher(Song::Source source, QObject *parent)
ReloadSettings();
QObject::connect(fs_watcher_, &FileSystemWatcherInterface::PathChanged, this, &CollectionWatcher::DirectoryChanged, Qt::UniqueConnection);
QObject::connect(rescan_timer_, &QTimer::timeout, this, &CollectionWatcher::RescanPathsNow);
QObject::connect(periodic_scan_timer_, &QTimer::timeout, this, &CollectionWatcher::IncrementalScanCheck);
}
void CollectionWatcher::ExitAsync() {
QMetaObject::invokeMethod(this, "Exit", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, &CollectionWatcher::Exit, Qt::QueuedConnection);
}
void CollectionWatcher::Exit() {
@@ -131,7 +131,7 @@ void CollectionWatcher::Exit() {
void CollectionWatcher::ReloadSettingsAsync() {
QMetaObject::invokeMethod(this, "ReloadSettings", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, &CollectionWatcher::ReloadSettings, Qt::QueuedConnection);
}
@@ -143,7 +143,7 @@ void CollectionWatcher::ReloadSettings() {
scan_on_startup_ = s.value("startup_scan", true).toBool();
monitor_ = s.value("monitor", true).toBool();
QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList();
if (source_ == Song::Source_Collection) {
if (source_ == Song::Source::Collection) {
song_tracking_ = s.value("song_tracking", false).toBool();
mark_songs_unavailable_ = song_tracking_ ? true : s.value("mark_songs_unavailable", true).toBool();
}
@@ -156,10 +156,10 @@ void CollectionWatcher::ReloadSettings() {
overwrite_rating_ = s.value("overwrite_rating", false).toBool();
s.endGroup();
best_image_filters_.clear();
best_art_filters_.clear();
for (const QString &filter : filters) {
QString str = filter.trimmed();
if (!str.isEmpty()) best_image_filters_ << str;
if (!str.isEmpty()) best_art_filters_ << str;
}
if (!monitor_ && was_monitoring_before) {
@@ -239,7 +239,7 @@ void CollectionWatcher::ScanTransaction::AddToProgressMax(const quint64 n) {
void CollectionWatcher::ScanTransaction::CommitNewOrUpdatedSongs() {
if (!deleted_songs.isEmpty()) {
if (mark_songs_unavailable_ && watcher_->source() == Song::Source_Collection) {
if (mark_songs_unavailable_ && watcher_->source() == Song::Source::Collection) {
emit watcher_->SongsUnavailable(deleted_songs);
}
else {
@@ -539,8 +539,8 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
bool changed = (matching_song.mtime() != qMax(fileinfo.lastModified().toSecsSinceEpoch(), matching_song_cue_mtime)) || cue_deleted || cue_added || cue_changed;
// Also want to look to see whether the album art has changed
QUrl image = ImageForSong(file, album_art);
if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) || (!matching_song.art_automatic().isEmpty() && !matching_song.has_embedded_cover() && !QFile::exists(matching_song.art_automatic().toLocalFile()))) {
const QUrl art_automatic = ArtForSong(file, album_art);
if (matching_song.art_automatic() != art_automatic || (!matching_song.art_automatic().isEmpty() && !matching_song.art_automatic_is_valid())) {
changed = true;
}
@@ -573,16 +573,16 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
#endif
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, image, cue_deleted, t);
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, cue_deleted, t);
}
else { // If CUE associated.
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, image, matching_songs, t);
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
}
}
// Nothing has changed - mark the song available without re-scanning
else if (matching_song.is_unavailable()) {
else if (matching_song.unavailable()) {
t->readded_songs << matching_songs;
}
@@ -633,13 +633,13 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
}
// Get new album art
QUrl image = ImageForSong(file, album_art);
const QUrl art_automatic = ArtForSong(file, album_art);
if (new_cue.isEmpty() || new_cue_mtime == 0) { // If no CUE or it's about to lose it.
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, image, matching_songs_has_cue && new_cue_mtime == 0, t);
UpdateNonCueAssociatedSong(file, fingerprint, matching_songs, art_automatic, matching_songs_has_cue && new_cue_mtime == 0, t);
}
else { // If CUE associated.
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, image, matching_songs, t);
UpdateCueAssociatedSongs(file, path, fingerprint, new_cue, art_automatic, matching_songs, t);
}
}
@@ -653,12 +653,12 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
qLog(Debug) << file << "is new.";
// Choose an image for the song(s)
QUrl image = ImageForSong(file, album_art);
// Choose art for the song(s)
const QUrl art_automatic = ArtForSong(file, album_art);
for (Song song : songs) {
song.set_directory_id(t->dir());
if (song.art_automatic().isEmpty()) song.set_art_automatic(image);
if (song.art_automatic().isEmpty()) song.set_art_automatic(art_automatic);
t->new_songs << song;
}
}
@@ -669,7 +669,7 @@ void CollectionWatcher::ScanSubdirectory(const QString &path, const CollectionSu
// Look for deleted songs
for (const Song &song : songs_in_db) {
QString file = song.url().toLocalFile();
if (!song.is_unavailable() && !files_on_disk.contains(file) && !t->files_changed_path_.contains(file)) {
if (!song.unavailable() && !files_on_disk.contains(file) && !t->files_changed_path_.contains(file)) {
qLog(Debug) << "Song deleted from disk:" << file;
t->deleted_songs << song;
}
@@ -704,7 +704,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
const QString &path,
const QString &fingerprint,
const QString &matching_cue,
const QUrl &image,
const QUrl &art_automatic,
const SongList &old_cue_songs,
ScanTransaction *t) {
@@ -733,7 +733,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
if (sections_map.contains(new_cue_song.beginning_nanosec())) { // Changed section
const Song matching_cue_song = sections_map[new_cue_song.beginning_nanosec()];
new_cue_song.set_id(matching_cue_song.id());
if (!new_cue_song.has_embedded_cover()) new_cue_song.set_art_automatic(image);
new_cue_song.set_art_automatic(art_automatic);
new_cue_song.MergeUserSetData(matching_cue_song, true, true);
AddChangedSong(file, matching_cue_song, new_cue_song, t);
used_ids.insert(matching_cue_song.id());
@@ -755,7 +755,7 @@ void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file,
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
const QString &fingerprint,
const SongList &matching_songs,
const QUrl &image,
const QUrl &art_automatic,
const bool cue_deleted,
ScanTransaction *t) {
@@ -763,7 +763,7 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
const Song &matching_song = matching_songs.first();
if (cue_deleted) {
for (const Song &song : matching_songs) {
if (!song.IsMetadataAndMoreEqual(matching_song)) {
if (!song.IsAllMetadataEqual(matching_song)) {
t->deleted_songs << song;
}
}
@@ -776,7 +776,7 @@ void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file,
song_on_disk.set_directory_id(t->dir());
song_on_disk.set_id(matching_song.id());
song_on_disk.set_fingerprint(fingerprint);
if (!song_on_disk.has_embedded_cover()) song_on_disk.set_art_automatic(image);
song_on_disk.set_art_automatic(art_automatic);
song_on_disk.MergeUserSetData(matching_song, !overwrite_playcount_, !overwrite_rating_);
AddChangedSong(file, matching_song, song_on_disk, t);
}
@@ -838,7 +838,7 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
bool notify_new = false;
QStringList changes;
if (matching_song.is_unavailable()) {
if (matching_song.unavailable()) {
qLog(Debug) << "unavailable song" << file << "restored.";
notify_new = true;
}
@@ -855,10 +855,26 @@ void CollectionWatcher::AddChangedSong(const QString &file, const Song &matching
changes << "metadata";
notify_new = true;
}
if (matching_song.art_automatic() != new_song.art_automatic() || matching_song.art_manual() != new_song.art_manual()) {
if (!matching_song.IsPlayStatisticsEqual(new_song)) {
changes << "play statistics";
notify_new = true;
}
if (!matching_song.IsRatingEqual(new_song)) {
changes << "rating";
notify_new = true;
}
if (!matching_song.IsArtEqual(new_song)) {
changes << "album art";
notify_new = true;
}
if (!matching_song.IsAcoustIdEqual(new_song)) {
changes << "acoustid";
notify_new = true;
}
if (!matching_song.IsMusicBrainzEqual(new_song)) {
changes << "musicbrainz";
notify_new = true;
}
if (matching_song.mtime() != new_song.mtime()) {
changes << "mtime";
}
@@ -901,7 +917,6 @@ void CollectionWatcher::AddWatch(const CollectionDirectory &dir, const QString &
if (!QFile::exists(path)) return;
QObject::connect(fs_watcher_, &FileSystemWatcherInterface::PathChanged, this, &CollectionWatcher::DirectoryChanged, Qt::UniqueConnection);
fs_watcher_->AddPath(path);
subdir_mapping_[path] = dir;
@@ -1024,20 +1039,20 @@ void CollectionWatcher::RescanPathsNow() {
}
QString CollectionWatcher::PickBestImage(const QStringList &images) {
QString CollectionWatcher::PickBestArt(const QStringList &art_automatic_list) {
// This is used when there is more than one image in a directory.
// Pick the biggest image that matches the most important filter
QStringList filtered;
for (const QString &filter_text : best_image_filters_) {
for (const QString &filter_text : best_art_filters_) {
// The images in the images list are represented by a full path, so we need to isolate just the filename
for (const QString &image : images) {
QFileInfo fileinfo(image);
for (const QString &art_automatic : art_automatic_list) {
QFileInfo fileinfo(art_automatic);
QString filename(fileinfo.fileName());
if (filename.contains(filter_text, Qt::CaseInsensitive))
filtered << image;
filtered << art_automatic;
}
// We assume the filters are give in the order best to worst, so if we've got a result, we go with it.
@@ -1047,7 +1062,7 @@ QString CollectionWatcher::PickBestImage(const QStringList &images) {
if (filtered.isEmpty()) {
// The filter was too restrictive, just use the original list
filtered = images;
filtered = art_automatic_list;
}
int biggest_size = 0;
@@ -1070,20 +1085,21 @@ QString CollectionWatcher::PickBestImage(const QStringList &images) {
}
QUrl CollectionWatcher::ImageForSong(const QString &path, QMap<QString, QStringList> &album_art) {
QUrl CollectionWatcher::ArtForSong(const QString &path, QMap<QString, QStringList> &art_automatic_list) {
QString dir(DirectoryPart(path));
if (album_art.contains(dir)) {
if (album_art[dir].count() == 1) {
return QUrl::fromLocalFile(album_art[dir][0]);
if (art_automatic_list.contains(dir)) {
if (art_automatic_list[dir].count() == 1) {
return QUrl::fromLocalFile(art_automatic_list[dir][0]);
}
else {
QString best_image = PickBestImage(album_art[dir]);
album_art[dir] = QStringList() << best_image;
return QUrl::fromLocalFile(best_image);
const QString best_art = PickBestArt(art_automatic_list[dir]);
art_automatic_list[dir] = QStringList() << best_art;
return QUrl::fromLocalFile(best_art);
}
}
return QUrl();
}
@@ -1103,25 +1119,13 @@ void CollectionWatcher::SetRescanPaused(bool pause) {
void CollectionWatcher::IncrementalScanAsync() {
QMetaObject::invokeMethod(this, "IncrementalScanNow", Qt::QueuedConnection);
QMetaObject::invokeMethod(this, &CollectionWatcher::IncrementalScanNow, Qt::QueuedConnection);
}
void CollectionWatcher::FullScanAsync() {
QMetaObject::invokeMethod(this, "FullScanNow", Qt::QueuedConnection);
}
void CollectionWatcher::RescanTracksAsync(const SongList &songs) {
// Is List thread safe? if not, this may crash.
song_rescan_queue_.append(songs);
// Call only if it's not already running
if (!rescan_in_progress_) {
QMetaObject::invokeMethod(this, "RescanTracksNow", Qt::QueuedConnection);
}
QMetaObject::invokeMethod(this, &CollectionWatcher::FullScanNow, Qt::QueuedConnection);
}
@@ -1139,34 +1143,6 @@ void CollectionWatcher::IncrementalScanNow() { PerformScan(true, false); }
void CollectionWatcher::FullScanNow() { PerformScan(false, true); }
void CollectionWatcher::RescanTracksNow() {
Q_ASSERT(!rescan_in_progress_);
stop_requested_ = false;
// Currently we are too stupid to rescan one file at a time, so we'll just scan the full directories
QStringList scanned_dirs; // To avoid double scans
while (!song_rescan_queue_.isEmpty()) {
if (stop_requested_ || abort_requested_) break;
Song song = song_rescan_queue_.takeFirst();
QString songdir = song.url().toLocalFile().section('/', 0, -2);
if (!scanned_dirs.contains(songdir)) {
qLog(Debug) << "Song" << song.title() << "dir id" << song.directory_id() << "dir" << songdir;
ScanTransaction transaction(this, song.directory_id(), false, false, mark_songs_unavailable_);
quint64 files_count = FilesCountForPath(&transaction, songdir);
ScanSubdirectory(songdir, CollectionSubdirectory(), files_count, &transaction);
scanned_dirs << songdir;
emit CompilationsNeedUpdating();
}
else {
qLog(Debug) << "Directory" << songdir << "already scanned - skipping.";
}
}
Q_ASSERT(song_rescan_queue_.isEmpty());
rescan_in_progress_ = false;
}
void CollectionWatcher::PerformScan(const bool incremental, const bool ignore_mtimes) {
stop_requested_ = false;
@@ -1252,3 +1228,34 @@ quint64 CollectionWatcher::FilesCountForSubdirs(ScanTransaction *t, const Collec
return i;
}
void CollectionWatcher::RescanSongsAsync(const SongList &songs) {
QMetaObject::invokeMethod(this, "RescanSongs", Qt::QueuedConnection, Q_ARG(SongList, songs));
}
void CollectionWatcher::RescanSongs(const SongList &songs) {
stop_requested_ = false;
QStringList scanned_paths;
for (const Song &song : songs) {
if (stop_requested_ || abort_requested_) break;
const QString song_path = song.url().toLocalFile().section('/', 0, -2);
if (scanned_paths.contains(song_path)) continue;
ScanTransaction transaction(this, song.directory_id(), false, true, mark_songs_unavailable_);
CollectionSubdirectoryList subdirs(transaction.GetAllSubdirs());
for (const CollectionSubdirectory &subdir : subdirs) {
if (stop_requested_ || abort_requested_) break;
if (subdir.path != song_path) continue;
qLog(Debug) << "Rescan for directory ID" << song.directory_id() << "directory" << subdir.path;
quint64 files_count = FilesCountForPath(&transaction, subdir.path);
ScanSubdirectory(song_path, subdir, files_count, &transaction);
scanned_paths << subdir.path;
}
}
emit CompilationsNeedUpdating();
}

View File

@@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2023, 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
@@ -59,7 +59,6 @@ class CollectionWatcher : public QObject {
void IncrementalScanAsync();
void FullScanAsync();
void RescanTracksAsync(const SongList &songs);
void SetRescanPausedAsync(const bool pause);
void ReloadSettingsAsync();
@@ -68,19 +67,21 @@ class CollectionWatcher : public QObject {
void ExitAsync();
void RescanSongsAsync(const SongList &songs);
signals:
void NewOrUpdatedSongs(SongList);
void SongsMTimeUpdated(SongList);
void SongsDeleted(SongList);
void SongsUnavailable(SongList songs, bool unavailable = true);
void SongsReadded(SongList songs, bool unavailable = false);
void SubdirsDiscovered(CollectionSubdirectoryList subdirs);
void SubdirsMTimeUpdated(CollectionSubdirectoryList subdirs);
void NewOrUpdatedSongs(const SongList &songs);
void SongsMTimeUpdated(const SongList &songs);
void SongsDeleted(const SongList &songs);
void SongsUnavailable(const SongList &songs, const bool unavailable = true);
void SongsReadded(const SongList &songs, const bool unavailable = false);
void SubdirsDiscovered(const CollectionSubdirectoryList &subdirs);
void SubdirsMTimeUpdated(const CollectionSubdirectoryList &subdirs);
void CompilationsNeedUpdating();
void UpdateLastSeen(int directory_id, int expire_unavailable_songs_days);
void UpdateLastSeen(const int directory_id, const int expire_unavailable_songs_days);
void ExitFinished();
void ScanStarted(int task_id);
void ScanStarted(const int task_id);
public slots:
void AddDirectory(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdirs);
@@ -166,9 +167,9 @@ class CollectionWatcher : public QObject {
void IncrementalScanCheck();
void IncrementalScanNow();
void FullScanNow();
void RescanTracksNow();
void RescanPathsNow();
void ScanSubdirectory(const QString &path, const CollectionSubdirectory &subdir, const quint64 files_count, CollectionWatcher::ScanTransaction *t, const bool force_noincremental = false);
void RescanSongs(const SongList &songs);
private:
static bool FindSongsByPath(const SongList &songs, const QString &path, SongList *out);
@@ -177,17 +178,17 @@ class CollectionWatcher : public QObject {
inline static QString NoExtensionPart(const QString &fileName);
inline static QString ExtensionPart(const QString &fileName);
inline static QString DirectoryPart(const QString &fileName);
QString PickBestImage(const QStringList &images);
QUrl ImageForSong(const QString &path, QMap<QString, QStringList> &album_art);
QString PickBestArt(const QStringList &art_automatic_list);
QUrl ArtForSong(const QString &path, QMap<QString, QStringList> &art_automatic_list);
void AddWatch(const CollectionDirectory &dir, const QString &path);
void RemoveWatch(const CollectionDirectory &dir, const CollectionSubdirectory &subdir);
static quint64 GetMtimeForCue(const QString &cue_path);
void PerformScan(const bool incremental, const bool ignore_mtimes);
// Updates the sections of a cue associated and altered (according to mtime) media file during a scan.
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &image, const SongList &old_cue_songs, ScanTransaction *t);
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, const QUrl &art_automatic, const SongList &old_cue_songs, ScanTransaction *t);
// Updates a single non-cue associated and altered (according to mtime) song during a scan.
void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &image, const bool cue_deleted, ScanTransaction *t);
void UpdateNonCueAssociatedSong(const QString &file, const QString &fingerprint, const SongList &matching_songs, const QUrl &art_automatic, const bool cue_deleted, ScanTransaction *t);
// Scans a single media file that's present on the disk but not yet in the collection.
// It may result in a multiple files added to the collection when the media file has many sections (like a CUE related media file).
SongList ScanNewFile(const QString &file, const QString &path, const QString &fingerprint, const QString &matching_cue, QSet<QString> *cues_processed);
@@ -209,9 +210,9 @@ class CollectionWatcher : public QObject {
QThread *original_thread_;
QHash<QString, CollectionDirectory> subdir_mapping_;
// A list of words use to try to identify the (likely) best image found in an directory to use as cover artwork.
// A list of words use to try to identify the (likely) best album cover art found in an directory to use as cover artwork.
// e.g. using ["front", "cover"] would identify front.jpg and exclude back.jpg.
QStringList best_image_filters_;
QStringList best_art_filters_;
bool scan_on_startup_;
bool monitor_;
@@ -223,7 +224,6 @@ class CollectionWatcher : public QObject {
bool stop_requested_;
bool abort_requested_;
bool rescan_in_progress_; // True if RescanTracksNow() has been called and is working.
QMap<int, CollectionDirectory> watched_dirs_;
QTimer *rescan_timer_;
@@ -237,8 +237,6 @@ class CollectionWatcher : public QObject {
static QStringList sValidImages;
SongList song_rescan_queue_; // Set by UI thread
qint64 last_scan_time_;
};

View File

@@ -74,26 +74,26 @@ GroupByDialog::GroupByDialog(QWidget *parent) : QDialog(parent), ui_(new Ui_Grou
ui_->setupUi(this);
Reset();
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_None, 0));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Artist, 1));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_AlbumArtist, 2));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Album, 3));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_AlbumDisc, 4));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Disc, 5));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Format, 6));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Genre, 7));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Year, 8));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_YearAlbum, 9));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_YearAlbumDisc, 10));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_OriginalYear, 11));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_OriginalYearAlbum, 12));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Composer, 13));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Performer, 14));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Grouping, 15));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_FileType, 16));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Samplerate, 17));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Bitdepth, 18));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Bitrate, 19));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::None, 0));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Artist, 1));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::AlbumArtist, 2));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Album, 3));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::AlbumDisc, 4));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Disc, 5));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Format, 6));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Genre, 7));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Year, 8));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::YearAlbum, 9));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::YearAlbumDisc, 10));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::OriginalYear, 11));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::OriginalYearAlbum, 12));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Composer, 13));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Performer, 14));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Grouping, 15));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::FileType, 16));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Samplerate, 17));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Bitdepth, 18));
p_->mapping_.insert(Mapping(CollectionModel::GroupBy::Bitrate, 19));
QObject::connect(ui_->buttonbox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, &GroupByDialog::Reset);

View File

@@ -49,7 +49,7 @@ class GroupByDialog : public QDialog {
void accept() override;
signals:
void Accepted(CollectionModel::Grouping g, bool separate_albums_by_grouping);
void Accepted(const CollectionModel::Grouping g, const bool separate_albums_by_grouping);
private slots:
void Reset();

View File

@@ -36,6 +36,7 @@
#include "core/logging.h"
#include "core/iconloader.h"
#include "settings/collectionsettingspage.h"
#include "collectionmodel.h"
#include "savedgroupingmanager.h"
#include "ui_savedgroupingmanager.h"
@@ -83,68 +84,68 @@ QString SavedGroupingManager::GetSavedGroupingsSettingsGroup(const QString &sett
QString SavedGroupingManager::GroupByToString(const CollectionModel::GroupBy g) {
switch (g) {
case CollectionModel::GroupBy_None:
case CollectionModel::GroupByCount: {
case CollectionModel::GroupBy::None:
case CollectionModel::GroupBy::GroupByCount: {
return tr("None");
}
case CollectionModel::GroupBy_AlbumArtist: {
case CollectionModel::GroupBy::AlbumArtist: {
return tr("Album artist");
}
case CollectionModel::GroupBy_Artist: {
case CollectionModel::GroupBy::Artist: {
return tr("Artist");
}
case CollectionModel::GroupBy_Album: {
case CollectionModel::GroupBy::Album: {
return tr("Album");
}
case CollectionModel::GroupBy_AlbumDisc: {
case CollectionModel::GroupBy::AlbumDisc: {
return tr("Album - Disc");
}
case CollectionModel::GroupBy_YearAlbum: {
case CollectionModel::GroupBy::YearAlbum: {
return tr("Year - Album");
}
case CollectionModel::GroupBy_YearAlbumDisc: {
case CollectionModel::GroupBy::YearAlbumDisc: {
return tr("Year - Album - Disc");
}
case CollectionModel::GroupBy_OriginalYearAlbum: {
case CollectionModel::GroupBy::OriginalYearAlbum: {
return tr("Original year - Album");
}
case CollectionModel::GroupBy_OriginalYearAlbumDisc: {
case CollectionModel::GroupBy::OriginalYearAlbumDisc: {
return tr("Original year - Album - Disc");
}
case CollectionModel::GroupBy_Disc: {
case CollectionModel::GroupBy::Disc: {
return tr("Disc");
}
case CollectionModel::GroupBy_Year: {
case CollectionModel::GroupBy::Year: {
return tr("Year");
}
case CollectionModel::GroupBy_OriginalYear: {
case CollectionModel::GroupBy::OriginalYear: {
return tr("Original year");
}
case CollectionModel::GroupBy_Genre: {
case CollectionModel::GroupBy::Genre: {
return tr("Genre");
}
case CollectionModel::GroupBy_Composer: {
case CollectionModel::GroupBy::Composer: {
return tr("Composer");
}
case CollectionModel::GroupBy_Performer: {
case CollectionModel::GroupBy::Performer: {
return tr("Performer");
}
case CollectionModel::GroupBy_Grouping: {
case CollectionModel::GroupBy::Grouping: {
return tr("Grouping");
}
case CollectionModel::GroupBy_FileType: {
case CollectionModel::GroupBy::FileType: {
return tr("File type");
}
case CollectionModel::GroupBy_Format: {
case CollectionModel::GroupBy::Format: {
return tr("Format");
}
case CollectionModel::GroupBy_Samplerate: {
case CollectionModel::GroupBy::Samplerate: {
return tr("Sample rate");
}
case CollectionModel::GroupBy_Bitdepth: {
case CollectionModel::GroupBy::Bitdepth: {
return tr("Bit depth");
}
case CollectionModel::GroupBy_Bitrate: {
case CollectionModel::GroupBy::Bitrate: {
return tr("Bitrate");
}
}

View File

@@ -55,4 +55,6 @@
#cmakedefine USE_TAGLIB
#cmakedefine USE_TAGPARSER
#cmakedefine HAVE_QX11APPLICATION
#endif // CONFIG_H_IN

View File

@@ -54,16 +54,14 @@ ContextAlbum::ContextAlbum(QWidget *parent)
timeline_fade_(new QTimeLine(kFadeTimeLineMs, this)),
image_strawberry_(":/pictures/strawberry.png"),
image_original_(image_strawberry_),
pixmap_current_opacity_(1.0) {
pixmap_current_opacity_(1.0),
desired_height_(width()) {
setObjectName("context-widget-album");
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
cover_loader_options_.desired_height_ = width();
cover_loader_options_.pad_output_image_ = true;
cover_loader_options_.scale_output_image_ = true;
QImage image = ImageUtils::ScaleAndPad(image_strawberry_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
QImage image = ImageUtils::ScaleImage(image_strawberry_, QSize(desired_height_, desired_height_), devicePixelRatioF(), true);
if (!image.isNull()) {
pixmap_current_ = QPixmap::fromImage(image);
}
@@ -91,7 +89,7 @@ void ContextAlbum::Init(ContextView *context_view, AlbumCoverChoiceController *a
QSize ContextAlbum::sizeHint() const {
return QSize(pixmap_current_.width(), pixmap_current_.height());
return QSize(static_cast<int>(pixmap_current_.width() / devicePixelRatioF()), static_cast<int>(pixmap_current_.height() / devicePixelRatioF()));
}
@@ -128,8 +126,8 @@ void ContextAlbum::contextMenuEvent(QContextMenuEvent *e) {
void ContextAlbum::UpdateWidth(const int new_width) {
if (new_width != cover_loader_options_.desired_height_) {
cover_loader_options_.desired_height_ = new_width;
if (new_width != desired_height_) {
desired_height_ = new_width;
ScaleCover();
ScalePreviousCovers();
updateGeometry();
@@ -182,7 +180,7 @@ void ContextAlbum::DrawImage(QPainter *p, const QPixmap &pixmap, const qreal opa
if (qFuzzyCompare(opacity, static_cast<qreal>(0.0))) return;
p->setOpacity(opacity);
p->drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap);
p->drawPixmap(0, 0, static_cast<int>(pixmap.width() / pixmap.devicePixelRatioF()), static_cast<int>(pixmap.height() / pixmap.devicePixelRatioF()), pixmap);
}
@@ -235,7 +233,7 @@ void ContextAlbum::FadePreviousCoverFinished(std::shared_ptr<PreviousCover> prev
void ContextAlbum::ScaleCover() {
QImage image = ImageUtils::ScaleAndPad(image_original_, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
const QImage image = ImageUtils::ScaleImage(image_original_, QSize(desired_height_, desired_height_), devicePixelRatioF(), true);
if (image.isNull()) {
pixmap_current_ = QPixmap();
}
@@ -248,7 +246,7 @@ void ContextAlbum::ScaleCover() {
void ContextAlbum::ScalePreviousCovers() {
for (std::shared_ptr<PreviousCover> previous_cover : previous_covers_) {
QImage image = ImageUtils::ScaleAndPad(previous_cover->image, cover_loader_options_.scale_output_image_, cover_loader_options_.pad_output_image_, cover_loader_options_.desired_height_);
QImage image = ImageUtils::ScaleImage(previous_cover->image, QSize(desired_height_, desired_height_), devicePixelRatioF(), true);
if (image.isNull()) {
previous_cover->pixmap = QPixmap();
}

View File

@@ -33,8 +33,6 @@
#include <QPixmap>
#include <QMovie>
#include "covermanager/albumcoverloaderoptions.h"
class QMenu;
class QTimeLine;
class QPainter;
@@ -99,7 +97,6 @@ class ContextAlbum : public QWidget {
QMenu *menu_;
ContextView *context_view_;
AlbumCoverChoiceController *album_cover_choice_controller_;
AlbumCoverLoaderOptions cover_loader_options_;
bool downloading_covers_;
QTimeLine *timeline_fade_;
QImage image_strawberry_;
@@ -107,6 +104,7 @@ class ContextAlbum : public QWidget {
QPixmap pixmap_current_;
qreal pixmap_current_opacity_;
std::unique_ptr<QMovie> spinner_animation_;
int desired_height_;
};
#endif // CONTEXTALBUM_H

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